/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2010-2011  The Chromium OS Authors. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <ctype.h>
#include <errno.h>
#include <gdbus.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/assert.h>
#include <connman/element.h>
#include <connman/dbus.h>
#include <connman/device.h>
#include <connman/network.h>
#include <connman/inet.h>
#include <connman/log.h>
#include <connman/option.h>
#include <connman/plugin.h>

#define _DBG_MODEMMGR(fmt, arg...)	DBG(DBG_MODEM, fmt, ## arg)

#include <mm/mm-modem.h>

#include "mobile_provider.h"

#define DBUS_METHOD_GETALL        "GetAll"
#define DEFAULT_TIMEOUT_MS 5000
#define CONNECT_TIMEOUT_MS 45000
#define ACTIVATE_TIMEOUT_MS 120000
#define NETWORK_SCAN_TIMEOUT_MS 120000
#define NETWORK_REGISTER_TIMEOUT_MS 90000

#define MM_MODEM_ERROR(error)  MM_MODEM_INTERFACE "." error
#define MM_MOBILE_ERROR(error)  MM_MODEM_GSM_INTERFACE "." error

static const char *kErrorIncorrectPassword =
	MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_INCORRECTPASSWORD);
static const char *kErrorSimPinRequired =
	MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_SIMPINREQUIRED);
static const char *kErrorSimPukRequired =
	MM_MOBILE_ERROR( MM_ERROR_MODEM_GSM_SIMPUKREQUIRED);
static const char *kErrorGprsNotSubscribed =
	MM_MOBILE_ERROR(MM_ERROR_MODEM_GSM_GPRSNOTSUBSCRIBED);
static const char *kErrorConnected =
	MM_MODEM_ERROR(MM_ERROR_MODEM_CONNECTED);

#define CROMO_ERROR(error) "org.chromium.ModemManager.Error." error
static const char *kErrorOperationInitiated =
	CROMO_ERROR("OperationInitiated");

/* Name of the network */
#define NETWORK_NAME "CellularNetwork"

#define IP_METHOD_NONE (-1)
/*
 * For testing the mechanism for limiting connect
 * retries, change this to a small number.
 */
#define MAX_CONNECT_TRIES (9999)

/*
 * from mm-modem.h in ModemManager. This enum
 * should move into an XML file to become part
 * of the DBus API.
 */
typedef enum {
    MM_MODEM_STATE_UNKNOWN = 0,
    MM_MODEM_STATE_DISABLED = 10,
    MM_MODEM_STATE_DISABLING = 20,
    MM_MODEM_STATE_ENABLING = 30,
    MM_MODEM_STATE_ENABLED = 40,
    MM_MODEM_STATE_SEARCHING = 50,
    MM_MODEM_STATE_REGISTERED = 60,
    MM_MODEM_STATE_DISCONNECTING = 70,
    MM_MODEM_STATE_CONNECTING = 80,
    MM_MODEM_STATE_CONNECTED = 90,

    MM_MODEM_STATE_LAST = MM_MODEM_STATE_CONNECTED
} MMModemState;

enum {
	// Sugar to make calls to modem_connect_failed clearer
	MM_NO_RETRY = FALSE,
	MM_RETRY = TRUE,
};

/* Modem states */
enum modem_state {
	/**
	 * MODEM_STATE_UNINITIALIZED - Modem recently appeared
	 *
	 * Try to read state of modem from ModemManager
	 */
	MODEM_STATE_UNINITIALIZED,

	/**
	 * MODEM_STATE_DISABLED - Radio Off
	 *
	 * This state indicates that the modem radio is not turned on.
	 * This is the initial state off the modem.
	 */
	MODEM_STATE_DISABLED,

	/**
	 * MODEM_STATE_ENABLED - Modem enabled, radio on
	 *
	 * This state indicates that the modem radio is turned on, and
	 * it should be possible to measure signal strength
	 */
	MODEM_STATE_ENABLED,

	/**
	 * MODEM_STATE_REGISTERED
	 *
	 * This modem has registered with a network, has signal
	 * quality measurements.
	 */
	MODEM_STATE_REGISTERED,

	/**
	 * MODEM_STATE_CONNECTED
	 *
	 * This modem has connected to a network.
	 */
	MODEM_STATE_CONNECTED,

	/**
	 * MODEM_STATE_FAILED
	 *
	 * A terminal state.  Failed to read critical information from
	 * the modem.
	 */
	MODEM_STATE_FAILED,

	/**
	 * MODEM_STATE_REMOVED
	 *
	 * A terminal state.  Device is removed.  Fail all operations.
	 */
	MODEM_STATE_REMOVED,

};

enum modem_event {
	ME_NO_EVENT,
	ME_PROPERTIES_CHANGED,
	ME_SIGNAL_QUALITY_CHANGED,
	ME_REGISTRATION_STATE_CHANGED,
	ME_ENABLE_SUCCEEDED,
	ME_ENABLE_FAILED,
	ME_REGISTER_SUCCEEDED,
	ME_REGISTER_FAILED,
	ME_DISABLE_SUCCEEDED,
	ME_DISABLE_FAILED,
	ME_GETIP4CONFIG_SUCCEEDED,
	ME_GETIP4CONFIG_FAILED,
	ME_GETINFO_SUCCEEDED,
	ME_GETINFO_FAILED,
	ME_GET_PROPERTIES_SUCCEEDED,
	ME_GET_PROPERTIES_FAILED,
	ME_DISCONNECT_SUCCEEDED,
	ME_DISCONNECT_FAILED,
	ME_GET_SIGNALQUALITY_SUCCEEDED,
	ME_GET_SIGNALQUALITY_FAILED,
	ME_GET_SERVING_SYSTEM_SUCCEEDED,
	ME_GET_SERVING_SYSTEM_FAILED,
	ME_GET_REGISTRATION_STATE_SUCCEEDED,
	ME_GET_REGISTRATION_STATE_FAILED,

	ME_CREATE_DEVICE_FAILED,
};

enum apn_try_state {
	APN_TRY_START,
	APN_TRY_SAVED,
	APN_TRY_USER_APN,
	APN_TRY_LIST,
	APN_TRY_NULL_APN,
	APN_TRY_FAILED
};

static const char* kDefaultMMSearchPath =
  MM_MODEMMANAGER_SERVICE ":" MM_MODEMMANAGER_PATH
  ",org.chromium.ModemManager:/org/chromium/ModemManager";

struct modem_data;

static void modem_handle_event(struct modem_data *modem, enum modem_event event);
static void handle_reported_connect(struct modem_data *modem);
static void handle_reported_disconnect(struct modem_data *modem);
static const char *eventToString(enum modem_event);
static const char *stateToString(enum modem_state);
static enum connman_network_activation_state convert_activation_state(
		guint mm_activation_state);
static enum connman_element_error convert_activation_error(
		guint mm_activation_error);

typedef void (*modem_function)(struct modem_data *modem);
typedef enum connman_element_error (*error_conversion_func)(const DBusError *);

static int modem_connect_failed(struct modem_data *modem, connman_bool_t retry);
static int modem_set_connected(struct modem_data *modem, gboolean connected);

struct modem_method_callback {
	struct connman_dbus_method_callback *cb_data;
	struct modem_data *modem;
	error_conversion_func convert_error;
};

/*
 * A modem task contains the state necessary to move through a minor
 * state machine.  It is initialized with a list of functions that
 * need to be called in order to effect a transition.
 *
 * Tasks must do one of the following:
 *   send a DBUS message whose reply will later call modem_handle_event
 *   immediately call modem_handle_event()
 *
 * A modem object can only be running a single task at a time.
 */

struct modem_task_work {
	const char *name;
	modem_function functions[];
};

struct modem_task {
	gboolean remove_on_complete;	/* modem removed while task running */
	int index;		        /* state within this state machine */
	struct modem_task_work *work;
};

struct modem_data {
	char *owner;	      /* unique bus name of service owning this modem */
	char *service;	      /* name of the service owning this modem */
	char *dbus_path;
	GSList *pending_dbus_calls;
	struct connman_device *cm_device;

	/* Modem connection profile information */
	char *phone_number;             /* from a profile */
	gint type;			/* MM_MODEM_TYPE_xxx */
	char *meid;
	char *imei;
	char *imsi;

	enum modem_state state;		/* modem state */
	int scangen;			/* fake scan generation number */
	unsigned int signal_strength;	/* most recently reported */
	struct {
		guint evdo_registration_state;
		guint _1x_registration_state; /* 1xRTT network reg state */
		guint activation_state;
	} cdma;
	struct {
		struct mobile_provider *provider; /* Provider from the SIM */
		gint registration_state;	/* network registration state */
		char *network_id;		/* operator MCC/MNC as string */
		char *operator_name;		/* operator name reported in */
						/*     registration info */
		char *operator_country;		/* operator country from same */
		char *spn;                      /* service provider name from SIM */
		struct {
			gboolean enabled;	/* SIM PIN lock is enabled */
			char *unlock_required;	/* modem needs to be unlocked? */
			guint retries;		/* # unlock retries allowed */
		} lock;
		gint apn_index;			/* next APN in list to try */
		guint connect_tries;
		enum apn_try_state apn_try_state;
		struct connman_network_apn next_apn;
		gint id_index;
	} gsm;
	enum connman_network_cellular_technology
			network_technology;  /* EDGE, UMTS, etc. */
	struct {
		char *url;		/* online payment url */
		char *method;		/* online payment method */
		char *postdata;		/* online payment postdata */
	} olp;
	char *usage_url;                /* data usage url */
	char *device;
	char *driver;
	char *master_device;
	char *network_dbus_name;
	char *configured_carrier;	/* carrier for which modem is configured */
	gint ip_method;			/* MM_MODEM_IP_METHOD_xxx */

	gboolean pending_task; 		/* Is there a task pending? */
	gboolean disconnect_needed;	/* Disconnect before removing modem? */
	gboolean connect_needed;	/* modem manager reports connected at startup */
	struct modem_task task;		/* minor state machine */
};

struct modem_manager {
	int watch;		/* dbus watch */
	char *service;		/* service name for modem manager */
	char *path;		/* path for ModemManager Interface */
	char *bus_name;		/* unique bus name for the service */
};

gboolean imei_needed(struct modem_data *);
gboolean imsi_needed(struct modem_data *);
gboolean spn_needed(struct modem_data *);
gboolean msisdn_needed(struct modem_data *);

struct gsm_id_request {
	const char *name;
	const char *method;
	gboolean (*id_needed)(struct modem_data *);
} gsm_id_requests[] = {
	{"imei", MM_MODEM_GSM_CARD_METHOD_GETIMEI, imei_needed},
	{"imsi", MM_MODEM_GSM_CARD_METHOD_GETIMSI, imsi_needed},
	{"spn", MM_MODEM_GSM_CARD_METHOD_GETSPN, spn_needed},
	{"msisdn", MM_MODEM_GSM_CARD_METHOD_GETMSISDN, msisdn_needed},
	{NULL}
};

static struct mobile_provider_db *provider_db = NULL;
static GHashTable *modem_manager_watches = NULL;
static GHashTable *modem_hash = NULL;

static void modem_task_completed(struct modem_data **m)
{
	struct modem_data *modem = *m;
	if (modem->pending_task) {
		_DBG_MODEMMGR("%s: %s",
			      modem->dbus_path, modem->task.work->name);
	} else {
		_DBG_MODEMMGR("with no pending task");
	}
	modem->pending_task = FALSE;

	if (modem->task.remove_on_complete) {
		modem->state = MODEM_STATE_FAILED;
		g_hash_table_remove(modem_hash, modem->dbus_path);
		*m = NULL;
	}
};

/**
 * Run the next function in the task
 */
static void modem_task_run(struct modem_data *modem)
{
	struct modem_task *task = &modem->task;
	modem_function func;

	if (!modem->pending_task) {
		_DBG_MODEMMGR("%s has no active task", modem->dbus_path);
		return;
	}
	func = task->work->functions[task->index];
	if (func == 0 || modem->task.remove_on_complete) {
		modem_task_completed(&modem);
		return;
	}
	task->index++;
	func(modem);
}

/**
 * Rerun the current function in the task
 */
static void modem_task_rerun(struct modem_data *modem)
{
	struct modem_task *task = &modem->task;

	if (!modem->pending_task) {
		_DBG_MODEMMGR("%s has no active task", modem->dbus_path);
		return;
	}
	CONNMAN_ASSERT(task->index > 0);
	--task->index;
	modem_task_run(modem);
}

/**
 * Start running a new list of functions
 */
static int modem_task_run_new(struct modem_data *modem,
			      struct modem_task_work *work)
{
	struct modem_task *task = &modem->task;
	if (modem->pending_task) {
		_DBG_MODEMMGR("already running %s.  Rejecting %s",
			      task->work->name, work->name);
		return -ENXIO;  /* crosbug.com/8308 : this should be EAGAIN */
	}

	modem->pending_task = TRUE;

	task->work = work;
	task->index = 0;
	_DBG_MODEMMGR("%s: %s", modem->dbus_path, task->work->name);

	modem_task_run(modem);
	return -EINPROGRESS;
}

static void modem_create_device_task(struct modem_data *);
static void modem_disable_task(struct modem_data *);
static void modem_disconnect_task(struct modem_data *);
static void modem_enable_task(struct modem_data *);
static void modem_get_gsm_network_properties_task(struct modem_data *);
static void modem_get_gsm_card_properties_task(struct modem_data *);
static void modem_get_identifiers_task(struct modem_data *);
static void modem_get_info_task(struct modem_data *);
static void modem_get_properties_task(struct modem_data *);
static void modem_get_registration_state_task(struct modem_data *);
static void modem_get_status_task(struct modem_data *);
static void modem_register_task(struct modem_data *);
static void modem_report_enabled_task(struct modem_data *);

static struct modem_task_work initialize_modem_work = {
	"initialize", {
		modem_get_properties_task,
		modem_get_gsm_card_properties_task,
		modem_create_device_task,
		0}};

static struct modem_task_work enable_cdma_modem_work = {
	"enable_cdma", {
		modem_enable_task,
		modem_get_status_task,
		modem_get_identifiers_task,
		modem_get_info_task,
		modem_get_registration_state_task,
		modem_report_enabled_task,
		0}};

static struct modem_task_work enable_gsm_modem_work = {
	"enable_gsm", {
		modem_enable_task,
		modem_register_task,
		modem_get_status_task,
		modem_get_identifiers_task,
		modem_get_gsm_network_properties_task,
		modem_get_info_task,
		modem_get_registration_state_task,
		modem_report_enabled_task,
		0}};

/* If you ever adjust the contents of this list, please see the block comment in
 * modem_device_disable().
 */
static struct modem_task_work disable_connected_modem_work = {
	"disable_connected", {
		modem_disconnect_task,
		modem_disable_task,
	0}};

static struct modem_task_work disable_modem_work = {
	"disable", {
		modem_disable_task,
		0}};

/* If you ever adjust the contents of this list, please see the block comment in
 * modem_device_disable().
 */
static struct modem_task_work disconnect_modem_work = {
	"disconnect", {
		modem_disconnect_task,
		0}};

static DBusConnection *connection;
static const char *modem_managers;

static enum connman_element_error convert_error(const DBusError *error)
{
	if (!dbus_error_is_set(error))
		return CONNMAN_ELEMENT_ERROR_NO_ERROR;

	if (dbus_error_has_name(error, kErrorIncorrectPassword))
		return CONNMAN_ELEMENT_ERROR_INCORRECT_PIN;
	else if (dbus_error_has_name(error, kErrorSimPinRequired))
		return CONNMAN_ELEMENT_ERROR_PIN_REQUIRED;
	else if (dbus_error_has_name(error, kErrorSimPukRequired))
		return CONNMAN_ELEMENT_ERROR_PIN_BLOCKED;
	else
		return CONNMAN_ELEMENT_ERROR_INTERNAL;
}

static enum connman_element_error convert_register_error(const DBusError *error)
{
	if (!dbus_error_is_set(error))
		return CONNMAN_ELEMENT_ERROR_NO_ERROR;

	// TODO(ers) handle ModemManager errors
	return CONNMAN_ELEMENT_ERROR_INTERNAL;
}

/* ========================== */
/* Modem device object for 3G */
/* ========================== */

static struct modem_data *modem_device_find_modem(struct connman_device *device,
						  const char *func)
{
	const char *path = connman_device_get_string(device, "DBus.Object");
	if (path == NULL) {
		_DBG_MODEMMGR("%s: Cannot find modem for %p", func, device);
		return NULL;
	}
	return g_hash_table_lookup(modem_hash, path);
}


static int modem_device_probe(struct connman_device *device)
{
	_DBG_MODEMMGR("device %p", device);

	return 0;
}

static void modem_device_remove(struct connman_device *device)
{
	_DBG_MODEMMGR("device %p", device);
}


/**
 * Connman has requested we enable the modem.
 *
 */
static int modem_device_enable(struct connman_device *device)
{
	struct modem_data *modem;

	modem = modem_device_find_modem(device, __func__);
	if (modem == NULL) {
		return -ENODEV;
	}
	switch (modem->state) {
	case MODEM_STATE_DISABLED:
		switch (modem->type) {
		case MM_MODEM_TYPE_CDMA:
			return modem_task_run_new(modem,
						  &enable_cdma_modem_work);
		case MM_MODEM_TYPE_GSM:
			return modem_task_run_new(modem,
						  &enable_gsm_modem_work);
		}
		return -EIO;
	case MODEM_STATE_ENABLED:
	case MODEM_STATE_REGISTERED:
	case MODEM_STATE_CONNECTED:
		return 0;
	default:
		_DBG_MODEMMGR("Invalid state %s\n", stateToString(modem->state));
		return -EIO;
	}
}

/*
 * modem_device_disable
 *
 * Handle a request from connman to disable a modem (turn off the radio).
 */
static int modem_device_disable(struct connman_device *device)
{
	struct modem_data *modem;

	modem = modem_device_find_modem(device, __func__);
	_DBG_MODEMMGR("modem %p", modem);
	if (modem == NULL) {
		return -ENODEV;
	}
	switch (modem->state) {
	case MODEM_STATE_DISABLED:
	case MODEM_STATE_FAILED:
	case MODEM_STATE_REMOVED:
		/* It is already off, we are done */
		return 0;
	case MODEM_STATE_ENABLED:
	case MODEM_STATE_REGISTERED:
		return modem_task_run_new(modem,
					  &disable_modem_work);
	case MODEM_STATE_CONNECTED:
		/* Fixes crosbug.com/9371.
		 * That bug occurs when connman is asked (by the UI) to disable
		 * a connected device; connman's disable code first destroys all
		 * the device's networks (kicking off disconnects), then
		 * immediately tells us to disable the device. We'll notice that
		 * we're mid-disconnect and fail to start the disable_connected
		 * task, so instead we just switch over to the disable_connected
		 * task from the disconnect task.
		 *
		 * Important safety tip: If the disable_connected and disconnect
		 * tasks ever get out of sync, this will cause subtle
		 * misbehaviors under certain race conditions, like certain
		 * steps getting executed twice or not at all. If you change one
		 * of those lists, remember that you may need to tweak the index
		 * here.
		 */
		if (modem->task.work == &disconnect_modem_work) {
			_DBG_MODEMMGR("switching states...");
			/* The task index is always incremented before running
			 * the next task, so if it's <= 0, we're in real
			 * trouble...
			 */
			CONNMAN_ASSERT(modem->task.index > 0);
			CONNMAN_ASSERT(disconnect_modem_work.functions[modem->task.index - 1] ==
			               disable_connected_modem_work.functions[modem->task.index - 1]);
			modem->task.work = &disable_connected_modem_work;
			return 0;
		}
		return modem_task_run_new(modem,
					  &disable_connected_modem_work);
	default:
		_DBG_MODEMMGR("Invalid state %s\n", stateToString(modem->state));
		return -EIO;
	}
}

static int modem_device_require_pin(struct connman_device *device,
			    const char *pin, connman_bool_t require,
			    struct connman_dbus_method_callback *callback);
static int modem_device_enter_pin(struct connman_device *device,
			  const char *pin,
			  struct connman_dbus_method_callback *callback);
static int modem_device_unblock_pin(struct connman_device *device,
			    const char *unblock_code, const char *pin,
			    struct connman_dbus_method_callback *callback);
static int modem_device_change_pin(struct connman_device *device,
			   const char *old_pin, const char *new_pin,
			   struct connman_dbus_method_callback *callback);
static int modem_device_scan(struct connman_device *device);
static int modem_device_register(struct connman_device *device,
				 const char *network_id,
				 struct connman_dbus_method_callback *callback);

static struct connman_device_driver modem_driver = {
	.name		= "modem",
	.type		= CONNMAN_DEVICE_TYPE_CELLULAR,
	.probe		= modem_device_probe,
	.remove		= modem_device_remove,
	.enable		= modem_device_enable,
	.disable	= modem_device_disable,
	.scan		= modem_device_scan,
	.register_on_network
			= modem_device_register,
	.require_pin	= modem_device_require_pin,
	.enter_pin	= modem_device_enter_pin,
	.unblock_pin	= modem_device_unblock_pin,
	.change_pin	= modem_device_change_pin
};

/* modem_dbus_build_message:
 * @destination: DBus destination char*
 * @interface: DBus interface char*
 * @path: DBus path char*
 * @message: pointer to DBusMessage*
 * @method: method to call
 *
 * Copy the path to path_copy and build up a call to the specified
 * modem method.
 */

static int modem_dbus_build_message(const char *destination,
				    const char *interface,
				    const char *path,
				    DBusMessage **message,
				    const char *method)
{
	*message = dbus_message_new_method_call(destination,
						path,
						interface,
						method);
	if (message == NULL) {
		return -ENOMEM;
	}
	dbus_message_set_auto_start(*message, FALSE);
	return 0;
}

/* The D-Bus API requires that the following variable be global. */
static dbus_int32_t modem_pending_call_slot = -1;

static DBusMessage *modem_dbus_steal_reply(DBusPendingCall *call)
{
	struct modem_data *modem;

	modem = dbus_pending_call_get_data(call, modem_pending_call_slot);
	CONNMAN_ASSERT(modem != NULL);
	CONNMAN_ASSERT(g_slist_find(modem->pending_dbus_calls, call));
	DBusMessage *reply = dbus_pending_call_steal_reply(call);
	dbus_pending_call_unref(call);
	modem->pending_dbus_calls =
			g_slist_remove(modem->pending_dbus_calls, call);
	return reply;
}

static void modem_dbus_cancel_pending_calls(struct modem_data *modem)
{
	GSList *item = modem->pending_dbus_calls;
	while (item != NULL) {
		_DBG_MODEMMGR("Cancelling pending call");
		dbus_pending_call_cancel(item->data);
		dbus_pending_call_unref(item->data);
		item = g_slist_next(item);
	}
	g_slist_free(modem->pending_dbus_calls);
	modem->pending_dbus_calls = NULL;
}

/**
 * modem_dbus_send_with_reply
 * @modem: the modem object
 * @message: pointer to DBusMessage*
 * @user_data: data for callback function
 * @function:  completion callback
 * @timeout_ms:  DBus timeout in milliseconds
 */

static int modem_dbus_send_with_reply(struct modem_data *modem,
				      DBusMessage *message,
				      void *user_data,
				      DBusPendingCallNotifyFunction function,
				      int timeout_ms)
{
	int success = FALSE;
	DBusPendingCall *call;

	_DBG_MODEMMGR("Sending %s.%s  timeout: %d",
		      dbus_message_get_interface(message),
		      dbus_message_get_member(message),
		      timeout_ms);
	if (dbus_connection_send_with_reply(connection, message,
					    &call, timeout_ms) == FALSE) {
		CONNMAN_ERROR("Failed to get interface");
		goto error;
	}
	if (call == NULL) {
		CONNMAN_ERROR("D-Bus connection not available");
		goto error;
	}
	dbus_pending_call_set_notify(call, function, user_data, NULL);
	dbus_pending_call_set_data(call, modem_pending_call_slot, modem, NULL);
	/*
	 * Remember the pending call. We hold a reference, in
	 * addition to the reference held by the DBus library.
	 */
	modem->pending_dbus_calls = g_slist_prepend(modem->pending_dbus_calls,
						    call);
	success = TRUE;
error:
	dbus_message_unref(message);
	if (success == FALSE) {
		return -EIO;
	}
	return 0;
}

/**
 * modem_dbus_send_message_valist:
 * @interface: DBus interface char*
 * @method: method to call
 * @func: function to call on reply.
 * @timeout_ms:  DBus timeout in milliseconds
 * @user_data: an object to be passed to the notify function
 * @event: event to handle on errror
 * @first_arg_type, ...  message arguments
 *
 * Send a DBUS message. The message arguments can be specified with
 * with <type>, <variable address> pairs, terminated by DBUS_TYPE_INVALID.
 * If any errors occur, signal the internal modem event.
 */
static int modem_dbus_send_message_valist(struct modem_data *modem,
					  const char *interface,
					  const char *method,
					  DBusPendingCallNotifyFunction func,
					  void *user_data,
					  int timeout_ms,
					  enum modem_event event,
					  int first_arg_type,
					  va_list args)
{
	DBusMessage *message;
	int err;

	err = modem_dbus_build_message(modem->owner, interface,
				       modem->dbus_path, &message, method);
	if (err < 0) {
		if (event != ME_NO_EVENT)
			modem_handle_event(modem, event);
		return err;
	}

	if (first_arg_type != DBUS_TYPE_INVALID) {
		dbus_message_append_args_valist(message, first_arg_type, args);
	}

	err = modem_dbus_send_with_reply(modem, message,
					 user_data, func, timeout_ms);
	if (err < 0 && event != ME_NO_EVENT)
		modem_handle_event(modem, event);
	return err;
}

/* Non-va_list version of modem_dbus_send_message_valist */
static int modem_dbus_send_message(struct modem_data *modem,
				   const char *interface,
				   const char *method,
				   DBusPendingCallNotifyFunction func,
				   enum modem_event event,
				   int first_arg_type, ...
	)
{
	int err;
	va_list var_args;
	va_start(var_args, first_arg_type);
	err = modem_dbus_send_message_valist(modem, interface, method, func,
					     modem, DEFAULT_TIMEOUT_MS,
					     event, first_arg_type, var_args);
	va_end(var_args);
	return err;
}

/* Non-va_list version of modem_dbus_send_message_valist */
static int modem_dbus_send_message_with_timeout(struct modem_data *modem,
						const char *interface,
						const char *method,
						DBusPendingCallNotifyFunction func,
						int timeout_ms,
						enum modem_event event,
						int first_arg_type, ...
	)
{
	int err;
	va_list var_args;
	va_start(var_args, first_arg_type);
	err = modem_dbus_send_message_valist(modem, interface, method, func,
					     modem, timeout_ms,
					     event, first_arg_type, var_args);
	va_end(var_args);
	return err;
}

/**
 * modem_dbus_handle_reply
 * @func - name of the function that called this, for debug messages
 * @call - the dbus call, contains the reply message, etc
 * @modem - the modem object
 * @success_event - move the state machine based on this event if
 *                  there are no errors.
 * @failure_event - move the state machine based on this event if
 *                  there are errors.
 * first_arg_type, ... arguments to extract from the reply.  These
 *                     typically should point to fields of the modem.
 *
 * Handle a dbus reply message by checking for DBUS errors, extracting
 * arguments, and then moving the state machine based on either the
 * success event or the failure event.
 */
static void modem_dbus_handle_reply(const char *func,
			     DBusPendingCall *call,
			     struct modem_data *modem,
			     enum modem_event success_event,
			     enum modem_event failure_event,
			     int first_arg_type, ...
	)
{
	DBusMessage *reply;
	DBusError error;
	enum modem_event event = success_event;
	va_list var_args;

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("%s: Failed steal reply", func);
		modem_handle_event(modem, failure_event);
		return;
	}

	dbus_error_init(&error);

	va_start(var_args, first_arg_type);

	if (dbus_set_error_from_message(&error, reply)) {
		/*
		 * OperationInitiated errors are always ignored
		 * along with the reply itself, with the expectation
		 * that a completion event of some kind will arrive
		 * later.
		 */
		if (dbus_error_has_name(&error, kErrorOperationInitiated)) {
			event = ME_NO_EVENT;
		} else {
			_DBG_MODEMMGR("%s: Unexpected error %s: %s",
				      func, error.name, error.message);
			event = failure_event;
		}
	} else if ((dbus_message_get_args_valist(reply, &error,
				   first_arg_type, var_args)) == FALSE) {
		if (dbus_error_is_set(&error) == TRUE) {
			_DBG_MODEMMGR("%s: Error %s: %s", func, error.name,
				      error.message);
			event = failure_event;
		}
	}
	va_end(var_args);
	dbus_error_free(&error);
	dbus_message_unref(reply);

	_DBG_MODEMMGR("%s: Event %s", func, eventToString(event));
	if (event != ME_NO_EVENT)
		modem_handle_event(modem, event);
}

/**
 * modem_dbus_handle_reply_with_pending
 * @call - the dbus call, contains the reply message, etc
 * @user_data - a pointer to the modem_data structure
 *
 * Handle a dbus reply message by checking for D-Bus errors,
 * and sending either an error msesage or a method reply message
 * back to the original caller who invoked the flimflam method.
 */
static void modem_dbus_handle_reply_with_pending(DBusPendingCall *call,
						 void *user_data)
{
	struct modem_method_callback *cb = user_data;
	DBusMessage *reply;
	DBusError dberror;
	enum connman_element_error error;

	_DBG_MODEMMGR("Reply for async operation");
	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("%s: Failed steal reply", __func__);
		connman_dbus_invoke_callback(
				cb->cb_data,
				CONNMAN_ELEMENT_ERROR_INTERNAL);
		g_free(cb);
		return;
	}

	dbus_error_init(&dberror);
	if (dbus_set_error_from_message(&dberror, reply)) {
		error = cb->convert_error(&dberror);
	} else {
		error = CONNMAN_ELEMENT_ERROR_NO_ERROR;
	}
	dbus_error_free(&dberror);
	dbus_message_unref(reply);
	connman_dbus_invoke_callback(cb->cb_data, error);
	g_free(cb);
}

/**
 * modem_dbus_send_message_with_pending:
 * @modem: the modem object
 * @callback: object to be passed to the reply handler function
 * @timeout_ms:  DBus timeout in milliseconds
 * @interface: DBus interface name
 * @method: method to call
 * @first_arg_type, ...  message arguments
 *
 * Send a DBUS message. The message arguments can be specified with
 * with <type>, <variable address> pairs, terminated by DBUS_TYPE_INVALID.
 * This differs from modem_dbus_send_message in that it is used when an
 * async flimflam method has been invoked, and a reply message must be
 * sent back to the caller on completion.
 */
static int modem_dbus_send_message_with_pending(struct modem_data *modem,
						void *callback,
						int timeout_ms,
						const char *interface,
						const char *method,
						int first_arg_type, ...)
{
	int err = 0;

	_DBG_MODEMMGR("modem %s method %s", modem->dbus_path, method);
	va_list var_args;
	va_start(var_args, first_arg_type);
	err = modem_dbus_send_message_valist(
			modem,
			interface,
			method,
			modem_dbus_handle_reply_with_pending,
			callback,
			timeout_ms,
			ME_NO_EVENT,
			first_arg_type, var_args);
	va_end(var_args);
	return err;
}

#define VALID_DBUS_CHAR(c) \
		(('A' <= (c) && (c) <= 'Z') ||   \
		 ('a' <= (c) && (c) <= 'z') ||   \
		 ('0' <= (c) && (c) <= '9') ||   \
		 ((c) == '_'))

/*
 * Convert all characters that aren't valid in D-Bus
 * names to underscore characters. Returned name must
 * be freed by the caller.
 */
static char *sanitize_dbus_name(const char *name)
{
	char *sani_name = g_strdup(name);
	char *cp;

	for (cp = sani_name; *cp; cp++) {
		if (!VALID_DBUS_CHAR(*cp))
			*cp = '_';
	}
	return sani_name;
}

static struct connman_network *get_network(struct modem_data *modem)
{
	if (modem->network_dbus_name == NULL)
		return NULL;
	return connman_device_get_network(modem->cm_device,
					  modem->network_dbus_name);
}

/* Create a network element */
static struct connman_network *create_network(struct modem_data *modem)
{
	static unsigned int netid = 0;
	char namebuf[sizeof(NETWORK_NAME) + 16];
	struct connman_network *network;
	struct connman_network_operator *home;
	int rc;
	int index;
	const char *net_display_name = NULL;

	_DBG_MODEMMGR("path %s", modem->dbus_path);

	home = connman_device_get_home_provider(modem->cm_device);
	if (home != NULL &&
	    modem->type == MM_MODEM_TYPE_GSM &&
	    (modem->gsm.registration_state ==
	     MM_MODEM_GSM_NETWORK_REG_STATUS_HOME))
		net_display_name = home->name;

	if (net_display_name == NULL) {
		if (modem->gsm.operator_name != NULL &&
		    strlen(modem->gsm.operator_name) != 0)
			net_display_name = modem->gsm.operator_name;
		else if (modem->configured_carrier != NULL)
			net_display_name = modem->configured_carrier;
		else if (modem->gsm.network_id != NULL) {
			snprintf(namebuf, sizeof(namebuf), "cellular_%s",
				 modem->gsm.network_id);
			net_display_name = namebuf;
		} else {
			snprintf(namebuf, sizeof(namebuf), "%s%d", NETWORK_NAME, netid);
			netid++;
			net_display_name = namebuf;
		}
	}
	if (modem->imsi != NULL && modem->type == MM_MODEM_TYPE_GSM)
		modem->network_dbus_name = sanitize_dbus_name(modem->imsi);
	else
		modem->network_dbus_name = sanitize_dbus_name(net_display_name);

	network = connman_device_get_network(modem->cm_device,
					     modem->network_dbus_name);
	if (network != NULL) {
		_DBG_MODEMMGR("return existing network object %p", network);
		return network;	/* XXX(ers) error? */
	}

	/*
	 * Assume a single network is available for a 3G modem.  This
	 * may change for multi-mode cards.
	 */
	network = connman_network_create(modem->network_dbus_name,
					 CONNMAN_NETWORK_TYPE_CELLULAR);

	if (network == NULL) {
		return NULL;
	}

	rc = connman_network_set_string(network, "DBus.Object",
                modem->dbus_path);
	if (rc < 0) {
		connman_network_unref(network);
		return NULL;
	}
	rc = connman_network_set_available(network, TRUE);
	if (rc < 0) {
		connman_network_unref(network);
		return NULL;
	}

	index = connman_device_get_index(modem->cm_device);
	connman_network_set_index(network, index);

	rc = connman_device_add_network(modem->cm_device, network);
	if (rc < 0) {
		connman_network_unref(network);
		return NULL;
	}
	connman_network_set_protocol(network, CONNMAN_NETWORK_PROTOCOL_IP);
	connman_network_set_name(network, net_display_name);
	connman_network_set_scangen(network, ++modem->scangen);
	connman_network_set_group(network, modem->network_dbus_name);
	connman_network_set_strength(network, modem->signal_strength);
	connman_network_set_registration_info(network,
					      modem->network_technology,
					      CONNMAN_NETWORK_ROAMING_STATE_UNKNOWN);
	if (modem->type == MM_MODEM_TYPE_CDMA) {
		connman_network_set_activation_state(network,
					     modem->cdma.activation_state,
					     CONNMAN_ELEMENT_ERROR_NO_ERROR);
		if (modem->configured_carrier != NULL) {
			struct connman_network_operator op = {
				.name = modem->configured_carrier,
				.code = "",
				.country = "us"};
			connman_network_set_operator(network, &op);
		}
	} else {
		connman_network_set_activation_state(network,
				     CONNMAN_NETWORK_ACTIVATION_STATE_ACTIVATED,
				     CONNMAN_ELEMENT_ERROR_NO_ERROR);
	}
	if (modem->gsm.operator_name != NULL || modem->gsm.network_id != NULL
            || modem->gsm.operator_country != NULL) {
		struct connman_network_operator op = {
			.name = modem->gsm.operator_name,
			.code = modem->gsm.network_id,
			.country = modem->gsm.operator_country};
		connman_network_set_operator(network, &op);
	}
	if (modem->olp.url)
		connman_network_set_olp_url(network, modem->olp.url,
					    modem->olp.method, modem->olp.postdata);
	if (modem->usage_url)
		connman_network_set_usage_url(network, modem->usage_url);

	return network;
}

/**
 * modem_connect_reply
 *
 * Handle the reply message from a DBUS_CONNECT call
 */
static void modem_connect_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	DBusMessage *reply;
	DBusError error;

	dbus_error_init(&error);

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		modem_connect_failed(modem, MM_NO_RETRY);
		return;
	}

	if (dbus_set_error_from_message(&error, reply)) {
		/*
		 * Some errors are optionally treated as acceptable,
		 * and handling of the reply is allowed to proceed.
		 * In addition OperationInitiated errors are always
		 * ignored along with the reply itself, with the
		 * expectation that a completion event of some kind
		 * will arrive later.
		 */
		_DBG_MODEMMGR("Connect returned error %s: %s",
				      error.name, error.message);
		if (dbus_error_has_name(&error, kErrorOperationInitiated))
			goto done;
		else if (dbus_error_has_name(&error, kErrorGprsNotSubscribed))
			modem_connect_failed(modem, MM_RETRY);
		else if (!dbus_error_has_name(&error, kErrorConnected))
			modem_connect_failed(modem, MM_NO_RETRY);
		else
			modem_set_connected(modem, TRUE);
	} else {
		modem_set_connected(modem, TRUE);
	}
done:
	dbus_error_free(&error);
	dbus_message_unref(reply);
	/* If we're roaming, we may not be allowed to have a data connection */
	if (modem->gsm.registration_state == MM_MODEM_GSM_NETWORK_REG_STATUS_ROAMING &&
	    !connman_device_roaming_allowed(modem->cm_device)) {
		connman_error("%s: disconnecting: "
			      "roaming connections not allowed",
			      __func__);
		modem_task_run_new(modem, &disconnect_modem_work);
	}
}

/**
 * modem_connect_start:
 * @modem: modem object
 *
 * Issue a DBUS message to connect the modem.
 *
 * For GSM modems, the APN is looked up in the mobile providers
 * database, but if the user has explicitly set the APN for
 * a network ID belonging the operator of the currently
 * registered network, the user-set APN is used instead of
 * the one from the database.
 */
static int modem_connect_start(struct modem_data *modem)
{
	DBusMessageIter dict, array;
	DBusMessage *message;
	connman_bool_t home_only;
	int rc = 0;
	struct connman_network *network = get_network(modem);

	connman_network_set_connecting(network);

	rc = modem_dbus_build_message(modem->owner, MM_MODEM_SIMPLE_INTERFACE,
				      modem->dbus_path, &message,
				      MM_MODEM_SIMPLE_METHOD_CONNECT);
	if (rc < 0) {
		modem_connect_failed(modem, MM_NO_RETRY);
		return rc;
	}

	dbus_message_iter_init_append(message, &array);

	dbus_message_iter_open_container(&array, DBUS_TYPE_ARRAY,
			DBUS_DICT_ENTRY_BEGIN_CHAR_AS_STRING
			DBUS_TYPE_STRING_AS_STRING DBUS_TYPE_VARIANT_AS_STRING
			DBUS_DICT_ENTRY_END_CHAR_AS_STRING, &dict);

	connman_dbus_dict_append_variant(&dict, "number",
					 DBUS_TYPE_STRING,
					 &modem->phone_number);

	if (modem->gsm.next_apn.apn != NULL) {
		connman_dbus_dict_append_variant(&dict, "apn",
						 DBUS_TYPE_STRING,
						 &modem->gsm.next_apn.apn);

		if (modem->gsm.next_apn.username != NULL) {
			connman_dbus_dict_append_variant(&dict, "username",
						 DBUS_TYPE_STRING,
						 &modem->gsm.next_apn.username);
		}

		if (modem->gsm.next_apn.password != NULL) {
			connman_dbus_dict_append_variant(&dict, "password",
						 DBUS_TYPE_STRING,
						 &modem->gsm.next_apn.password);
		}
	}
	home_only = !connman_device_roaming_allowed(modem->cm_device);
	if (home_only)
		connman_dbus_dict_append_variant(&dict, "home_only",
						 DBUS_TYPE_BOOLEAN,
						 &home_only);
	++modem->gsm.connect_tries;

	dbus_message_iter_close_container(&array, &dict);

	rc = modem_dbus_send_with_reply(modem, message,
					modem, modem_connect_reply,
					CONNECT_TIMEOUT_MS);
	if (rc < 0) {
		modem_connect_failed(modem, MM_NO_RETRY);
		return rc;
	}
	return -EINPROGRESS;
}

/**
 * For GSM modems, remember the APN that was used to successfully
 * connect, so that the same APN can be used next time without
 * having to try other APNs that we already know don't work.
 */
static void modem_record_connect_info(struct modem_data *modem)
{
	struct connman_network *network;

	if (modem->type != MM_MODEM_TYPE_GSM ||
	    modem->state != MODEM_STATE_CONNECTED)
		return;

	if (modem->gsm.next_apn.apn != NULL &&
	    modem->gsm.next_apn.apn[0] != '\0') {
		_DBG_MODEMMGR("Last successful APN: \"%s\" \"%s\" \"%s\"",
			      modem->gsm.next_apn.apn,
			      modem->gsm.next_apn.username,
			      modem->gsm.next_apn.password);
		network = get_network(modem);
		connman_network_save_last_good_apn(network, &modem->gsm.next_apn);
	} else {
		/* If the modem was already started when we started
		   up, then we might not have fetched a valid APN yet */
		_DBG_MODEMMGR("APN was null");
	}
}

static void modem_set_powered_failed(struct modem_data *modem,
				     enum connman_element_error error);
/**
 * modem_enable_reply:
 * @modem: modem object
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS enable(TRUE) message.
 */
static void modem_enable_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	DBusMessage *reply;
	DBusError error;
	enum modem_event event = ME_ENABLE_SUCCEEDED;

	_DBG_MODEMMGR("");
	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		modem_set_powered_failed(modem, convert_error(&error));
		modem_handle_event(modem, ME_ENABLE_FAILED);
		return;
	}

	dbus_error_init(&error);

	if (dbus_set_error_from_message(&error, reply)) {
		/*
		 * OperationInitiated errors are always ignored
		 * along with the reply itself, with the expectation
		 * that a completion event of some kind will arrive
		 * later.
		 */
		if (!dbus_error_has_name(&error, kErrorOperationInitiated)) {
			_DBG_MODEMMGR("Enable failed %s: %s",
				      error.name, error.message);
			modem_set_powered_failed(modem, convert_error(&error));
			event = ME_ENABLE_FAILED;
		}
	}
	dbus_error_free(&error);
	dbus_message_unref(reply);
	modem_handle_event(modem, event);
}


/**
 * modem_disable_reply:
 * @modem: modem object
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS enable(FALSE) message.
 */

static void modem_disable_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	modem_dbus_handle_reply(__func__, call, modem,
				ME_DISABLE_SUCCEEDED, ME_DISABLE_FAILED,
				DBUS_TYPE_INVALID /* no return values */);
}

/**
 * modem_enable_task:
 * @modem: modem object
 * @task: modem task object
 *
 * Send a DBUS message to enable a modem.  As a task, all failures
 * must be reported via modem_handle_event.
 */
static void modem_enable_task(struct modem_data *modem)
{
	int enable = TRUE;

	_DBG_MODEMMGR("enable modem %s", modem->dbus_path);
	modem_dbus_send_message(modem,
				MM_MODEM_INTERFACE, MM_MODEM_METHOD_ENABLE,
				modem_enable_reply, ME_ENABLE_FAILED,
				DBUS_TYPE_BOOLEAN, &enable,
				DBUS_TYPE_INVALID);
}


/**
 * modem_disable_task:
 * @modem: modem object
 * @task: modem task object
 *
 * Send a DBUS message to disable a modem.  As a task, all failures
 * must be reported via modem_handle_event.
 */
static void modem_disable_task(struct modem_data *modem)
{
	int enable = FALSE;

	_DBG_MODEMMGR("disable modem %s", modem->dbus_path);
	modem_dbus_send_message(modem,
				MM_MODEM_INTERFACE, MM_MODEM_METHOD_ENABLE,
				modem_disable_reply, ME_DISABLE_FAILED,
				DBUS_TYPE_BOOLEAN, &enable,
				DBUS_TYPE_INVALID);
}

static void modem_set_powered(struct modem_data *modem, gboolean powered);
static int modem_registration_state_change(struct modem_data *modem);

/**
 * modem_report_enabled_task
 * @modem: modem object
 *
 * Report to flimflam core that the modem is enabled.
 */
static void modem_report_enabled_task(struct modem_data *modem) {
	_DBG_MODEMMGR("");
	modem_set_powered(modem, TRUE);
	/* In case we deferred a registration state change while enabling */
	modem_registration_state_change(modem);
	modem_task_run(modem);
}

/**
 * Handle a change in activation state
 */
static void modem_update_activation_state(struct modem_data *modem,
	enum connman_network_activation_state state,
	enum connman_element_error err)
{
	struct connman_network *network = get_network(modem);
	if (network != NULL) {
		modem->cdma.activation_state = state;
		connman_network_set_activation_state(network, state, err);
	}
}

/**
 * modem_activate_reply:
 * @modem: modem object
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS activate message.  Certain errors are
 * indicative of the modem already being activated, so if that is the
 * case, we can proceed despite the error.
 */

static void modem_activate_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;
	uint32_t status = -1;
	DBusMessage *reply;
	DBusError error;

	dbus_error_init(&error);

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("%s: Failed steal reply", __func__);
		goto done_no_reply;
	}

	if (dbus_set_error_from_message(&error, reply)) {
		if (dbus_error_has_name(&error, kErrorOperationInitiated)) {
			/*
			 * Activation can take a long while.  We rely
			 * on a signal to indicate a future state
			 * change of the modem, or for the modem to
			 * disappear.  Nothing to do.
			 */
			dbus_message_unref(reply);
			return;
		}
		CONNMAN_ERROR("DBus Error: %s:%s",
			      error.name, error.message);
		goto done;
	}

	if (dbus_message_get_args(reply, &error,
				  DBUS_TYPE_UINT32,
				  &status,
				  DBUS_TYPE_INVALID) == FALSE) {
		if (dbus_error_is_set(&error) == TRUE) {
			CONNMAN_ERROR("Error retrieving reply argument: %s:%s",
				      error.name, error.message);
		} else {
			CONNMAN_ERROR("Reply argument type is not UINT32");
		}
	}
done:
	dbus_message_unref(reply);
done_no_reply:
	if (status != 0) {
		modem_update_activation_state(modem,
		     /* FIXME(jglasgow): need to leave state unchanged */
		     CONNMAN_NETWORK_ACTIVATION_STATE_NOT_ACTIVATED,
		     convert_activation_error(status));
	}
	/*
	 * For the case of status == 0, we rely either on an explicit
	 * signal of the new modem state, or we expect the modem to
	 * disappear, reset itself and come back
	 */
}


/**
 * modem_register_reply:
 * @modem: modem object
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS register message.
 */

static void modem_register_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	modem_dbus_handle_reply(__func__, call, modem,
				ME_REGISTER_SUCCEEDED, ME_REGISTER_FAILED,
				DBUS_TYPE_INVALID /* no return values */);
}

/**
 * modem_register_task:
 * @modem: modem object
 * @task: modem task object
 *
 * Send a DBUS message to register a GSM modem. As a task, all failures
 * must be reported via modem_handle_event.
 */
static void modem_register_task(struct modem_data *modem)
{
	const char *network_id;

	network_id = connman_device_get_selected_network(modem->cm_device);
	_DBG_MODEMMGR("register modem %s on %s", modem->dbus_path,
		      network_id ? network_id : "home network");
	if (network_id == NULL)
		network_id = "";	/* Use home network */
	modem_dbus_send_message_with_timeout(modem,
				MM_MODEM_GSM_NETWORK_INTERFACE,
				MM_MODEM_GSM_NETWORK_METHOD_REGISTER,
				modem_register_reply,
				NETWORK_REGISTER_TIMEOUT_MS,
				ME_REGISTER_FAILED,
				DBUS_TYPE_STRING, &network_id,
				DBUS_TYPE_INVALID);
}

static void free_dict(gpointer data)
{
	g_hash_table_destroy(data);
}

static GPtrArray *make_apn_list(struct mobile_provider *provider)
{
	int i, j;
	GPtrArray *apn_list;

	if (provider == NULL)
		return NULL;

	apn_list = g_ptr_array_sized_new(provider->num_apns);
	g_ptr_array_set_free_func(apn_list, free_dict);
	for (i = 0; i < provider->num_apns; ++i) {
		struct mobile_apn *apn = provider->apns[i];
		GHashTable *props = g_hash_table_new_full(g_str_hash,
							  g_str_equal,
							  NULL, g_free);
		struct localized_name *name = NULL;
		struct localized_name *lname = NULL;

		if (apn->value != NULL)
			g_hash_table_insert(props, "apn", g_strdup(apn->value));
		if (apn->username != NULL)
			g_hash_table_insert(props, "username",
					    g_strdup(apn->username));
		if (apn->password != NULL)
			g_hash_table_insert(props, "password",
					    g_strdup(apn->password));
		/*
		 * Find the first localized name, if any, and
		 * the first non-localized name, if any.
		 */
		for (j = 0; j < apn->num_names; ++j) {
			if (apn->names[j]->lang != NULL) {
				if (lname == NULL)
					lname = apn->names[j];
			} else if (name == NULL) {
				name = apn->names[j];
			}
		}
		if (name != NULL)
			g_hash_table_insert(props, "name", g_strdup(name->name));
		if (lname != NULL) {
			g_hash_table_insert(props, "localized_name",
					    g_strdup(lname->name));
			g_hash_table_insert(props, "language",
					    g_strdup(lname->lang));
		}
		g_ptr_array_add(apn_list, props);
	}
	return apn_list;
}

/**
 * set_gsm_provider:
 *
 * Utility routine to set the upper level information about the
 * cellular provider from the modem's IMSI.
 */
static void set_gsm_provider(struct modem_data *modem,
        struct connman_device *device)
{
	struct mobile_provider *provider = NULL;
	GPtrArray *apn_list;

        _DBG_MODEMMGR("");

        if (modem->imsi == NULL)
		return;

	_DBG_MODEMMGR("imsi '%s' spn '%s'", modem->imsi, modem->gsm.spn);
	provider = mobile_provider_lookup_best_match(provider_db,
						     modem->gsm.spn,
						     modem->imsi);
	if (provider != NULL) {
		modem->gsm.provider = provider;
		struct connman_network_operator op = {
			.code = provider->networks ? provider->networks[0] : NULL,
			.country = provider->country};
		if (modem->gsm.spn != NULL && strlen(modem->gsm.spn) != 0)
			op.name = g_strdup(modem->gsm.spn);
		else
			op.name = g_strdup(mobile_provider_get_name(provider));
		connman_device_set_home_provider(device, &op);
		g_free(op.name);
		apn_list = make_apn_list(provider);
		if (apn_list != NULL)
			connman_device_set_apn_list(device, apn_list);
		connman_device_set_provider_requires_roaming(device,
							     provider->requires_roaming);
	}
}

/**
 * modem_create_device_task - create a connman device object for a modem
 *
 */
static void modem_create_device_task(struct modem_data *modem)
{
	struct connman_device *device;
	int index;

	_DBG_MODEMMGR("path %s interface %s", modem->dbus_path,
		      modem->device);

	if (modem->ip_method != MM_MODEM_IP_METHOD_DHCP) {
		/* PPP is not supported */
		CONNMAN_ERROR("Unsupported modem IP method %d",
			      modem->ip_method);
		modem_handle_event(modem, ME_CREATE_DEVICE_FAILED);
		return;
	}
	index = connman_inet_ifindex(modem->device);
	device = connman_inet_create_device(index);
	if (device == NULL) {
		CONNMAN_ERROR("Cannot create modem device");
		modem_handle_event(modem, ME_CREATE_DEVICE_FAILED);
		return;
	}

	connman_device_set_string(device, "DBus.Object", modem->dbus_path);
	connman_device_set_string(device, "DBus.Service", modem->service);
	connman_device_set_string(device, "DBus.Connection", modem->owner);
	connman_device_set_string(device, "Tty", modem->device);
	if (modem->configured_carrier != NULL)
		connman_device_set_string(device, "Cellular.Carrier",
					  modem->configured_carrier);
	if (modem->meid != NULL)
		connman_device_set_string(device, "Cellular.MEID",
					  modem->meid);
	if (modem->imei != NULL)
		connman_device_set_string(device, "Cellular.IMEI",
					  modem->imei);
	if (modem->imsi != NULL)
		connman_device_set_string(device, "Cellular.IMSI",
					  modem->imsi);
        switch (modem->type) {
	case MM_MODEM_TYPE_CDMA:
		connman_device_set_cellular_family(device,
					   CONNMAN_DEVICE_CELLULAR_FAMILY_CDMA);
		/*
		 * TODO(jglasgow): phone number should come
		 * from configuration information
		 */
		modem->phone_number = g_strdup("#777");
		if (modem->configured_carrier != NULL) {
			/*
			 * Assume US for the country, since CDMA barely
			 * exists elsewhere (not actually correct, but
			 * it'll do for now).
			 */
			struct connman_network_operator op = {
				.name = modem->configured_carrier,
				.code = "",
				.country = "us"};
			connman_device_set_home_provider(device, &op);
			connman_device_set_string(device,
						  "Cellular.FirmwareImageName",
						  modem->configured_carrier);
		}
		break;
	case MM_MODEM_TYPE_GSM:
		connman_device_set_cellular_family(device,
					   CONNMAN_DEVICE_CELLULAR_FAMILY_GSM);
		modem->phone_number = g_strdup("*99#");
		set_gsm_provider(modem, device);
		connman_device_set_unlock_properties(device,
						     modem->gsm.lock.unlock_required,
						     modem->gsm.lock.retries,
						     modem->gsm.lock.enabled);
		break;
	}

	connman_device_set_scanning_supported(device,
					      modem->type == MM_MODEM_TYPE_GSM);
	/* We are now done with the creation, and we leave modem DISABLED */
	modem_task_completed(&modem);
	/* modem may have been freed by modem_task_completed() */
	if (modem == NULL)
		return;
	modem->state = MODEM_STATE_DISABLED;

	if (connman_device_register(device) < 0) {
		connman_device_unref(device);
		modem_handle_event(modem, ME_CREATE_DEVICE_FAILED);
		return;
	}
	modem->cm_device = device;
}

/**
 * modem_disconnect_reply:
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS disconnect message. by running the
 * state machine.
 */

static void modem_disconnect_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	modem_dbus_handle_reply(__func__, call, modem,
				ME_DISCONNECT_SUCCEEDED, ME_DISCONNECT_FAILED,
				DBUS_TYPE_INVALID /* no return values */);
}

/**
 * modem_disconnect_task
 * @modem: modem object
 *
 * Send a DBUS message to disconnect a modem
 */
static void modem_disconnect_task(struct modem_data *modem)
{
	modem_dbus_send_message(modem,
				MM_MODEM_INTERFACE,
				MM_MODEM_METHOD_DISCONNECT,
				modem_disconnect_reply,
				ME_DISCONNECT_FAILED,
				DBUS_TYPE_INVALID);
}

/**
 * modem_get_cdma_registration_state_reply
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS get_registration_state message.  Run
 * the state machine.
 */

static void modem_get_cdma_registration_state_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;

	modem_dbus_handle_reply(__func__, call, modem,
				ME_GET_REGISTRATION_STATE_SUCCEEDED,
				ME_GET_REGISTRATION_STATE_FAILED,
				DBUS_TYPE_UINT32, &modem->cdma._1x_registration_state,
				DBUS_TYPE_UINT32, &modem->cdma.evdo_registration_state,
				DBUS_TYPE_INVALID);

	if (modem->connect_needed)
		handle_reported_connect(modem);
}

static void set_network_id(struct modem_data *modem, const char *network_id)
{
	struct connman_network *network = get_network(modem);

	if (strlen(network_id) == 0)
		network_id = NULL;
	g_free(modem->gsm.network_id);
	modem->gsm.network_id = g_strdup(network_id);
	if (network_id != NULL) {
		const gchar *provider_name;
		struct mobile_provider *provider;
		provider = mobile_provider_lookup_by_network(provider_db,
							     network_id);
		if (provider == NULL)
			_DBG_MODEMMGR("GSM provider for network %s not found",
				      network_id);
		else {
			provider_name = mobile_provider_get_name(provider);
			_DBG_MODEMMGR("Found GSM provider %s for network %s",
				      provider_name, network_id);
			g_free(modem->gsm.operator_name);
			modem->gsm.operator_name = g_strdup(provider_name);
			g_free(modem->gsm.operator_country);
			modem->gsm.operator_country =
				g_strdup(provider->country);
		}
		if (network != NULL) {
			struct connman_network_operator op = {
				.name = modem->gsm.operator_name,
				.code = modem->gsm.network_id,
				.country = modem->gsm.operator_country};
			connman_network_set_operator(network, &op);
			if (modem->gsm.operator_name != NULL &&
			    strlen(modem->gsm.operator_name) != 0)
				connman_network_set_name(network,
						 modem->gsm.operator_name);
		}
	}
}

/**
 * modem_get_gsm_registration_state_reply
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS get_registration_state message.  Run
 * the state machine.
 */

static void modem_get_gsm_registration_state_reply(DBusPendingCall *call,
						   void *user_data)
{
	struct modem_data *modem = user_data;
	int failure_event = ME_GET_REGISTRATION_STATE_FAILED;
	gboolean do_connect = FALSE;

	DBusMessage *reply;
	DBusMessageIter reply_iter, values_iter;
	enum modem_event event = ME_GET_REGISTRATION_STATE_SUCCEEDED;
	gint type;
	const char *network_id = NULL, *operator_name = NULL;

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("%s: Failed steal reply", __func__);
		event = failure_event;
		goto done_no_reply;
	}

	if (dbus_message_iter_init(reply, &reply_iter) == FALSE) {
		CONNMAN_ERROR("%s: Could not iterate on reply", __func__);
		event = failure_event;
		goto done;
	}
	type = dbus_message_iter_get_arg_type(&reply_iter);
	if (type != DBUS_TYPE_STRUCT) {
		_DBG_MODEMMGR("Expected struct, got %x", type);
		event = failure_event;
		goto done;
	}
	dbus_message_iter_recurse(&reply_iter, &values_iter);
	dbus_message_iter_get_basic(&values_iter,
				    &modem->gsm.registration_state);
	dbus_message_iter_next(&values_iter);
	dbus_message_iter_get_basic(&values_iter, &network_id);
	dbus_message_iter_next(&values_iter);
	g_free(modem->gsm.operator_name);
	dbus_message_iter_get_basic(&values_iter, &operator_name);
	modem->gsm.operator_name = g_strdup(operator_name);
	set_network_id(modem, network_id);
	dbus_message_iter_next(&values_iter);
	do_connect = modem->connect_needed;
done:
	dbus_message_unref(reply);
done_no_reply:
	_DBG_MODEMMGR("%s: Event %s", __func__, eventToString(event));
	modem_handle_event(modem, event);
	if (do_connect) {
		handle_reported_connect(modem);
	}
}


/**
 * modem_get_registration_state_task
 * @modem: modem object
 *
 * Send a DBUS message to find the registration state/info of a modem
 */
static void modem_get_registration_state_task(struct modem_data *modem)
{
	switch (modem->type) {
	case MM_MODEM_TYPE_CDMA:
		modem_dbus_send_message(
				modem,
				MM_MODEM_CDMA_INTERFACE,
				MM_MODEM_CDMA_METHOD_GETREGISTRATIONSTATE,
				modem_get_cdma_registration_state_reply,
				ME_GET_REGISTRATION_STATE_FAILED,
				DBUS_TYPE_INVALID);
		return;
	case MM_MODEM_TYPE_GSM:
		modem_dbus_send_message(
				modem,
				MM_MODEM_GSM_NETWORK_INTERFACE,
				MM_MODEM_GSM_NETWORK_METHOD_GETREGISTRATIONINFO,
				modem_get_gsm_registration_state_reply,
				ME_GET_REGISTRATION_STATE_FAILED,
				DBUS_TYPE_INVALID);
		return;
	default:
		_DBG_MODEMMGR("Unknown modem type %d", modem->type);
		modem_handle_event(modem, ME_GET_REGISTRATION_STATE_FAILED);
		break;
	}
}


/**
 * modem_get_signal_quality_reply
 * @user_data: pointer to modem_task object
 *
 * Handle the reply from a DBUS message with the signal quality
 */

static void modem_get_signal_quality_reply(
	DBusPendingCall *call,
	void *user_data)
{
	struct modem_data *modem = user_data;

	modem_dbus_handle_reply(__func__, call, modem,
				ME_GET_SIGNALQUALITY_SUCCEEDED,
				ME_GET_SIGNALQUALITY_FAILED,
				DBUS_TYPE_UINT32, &modem->signal_strength,
				DBUS_TYPE_INVALID);
}


/**
 * modem_get_signal_quality
 * @modem: modem object
 *
 * Send a DBUS message to find the signal quality of a modem
 */
static void modem_get_signal_quality(struct modem_data *modem)
{
	switch (modem->type) {
	case MM_MODEM_TYPE_CDMA:
		modem_dbus_send_message(modem,
					MM_MODEM_CDMA_INTERFACE,
					MM_MODEM_CDMA_METHOD_GETSIGNALQUALITY,
					modem_get_signal_quality_reply,
					ME_GET_SIGNALQUALITY_FAILED,
					DBUS_TYPE_INVALID);
		return;
	case MM_MODEM_TYPE_GSM:
		modem_dbus_send_message(
				modem,
				MM_MODEM_GSM_NETWORK_INTERFACE,
				MM_MODEM_GSM_NETWORK_METHOD_GETSIGNALQUALITY,
				modem_get_signal_quality_reply,
				ME_GET_SIGNALQUALITY_FAILED,
				DBUS_TYPE_INVALID);
		return;
	default:
		_DBG_MODEMMGR("Unknown modem type %d", modem->type);
		modem_handle_event(modem, ME_GET_SIGNALQUALITY_FAILED);
		break;
	}
}


typedef void (*prop_reply_fn)(DBusPendingCall *, void *);

static void modem_get_properties(struct modem_data *modem,
				 const char *interface,
				 prop_reply_fn reply_fn)
{
	modem_dbus_send_message(modem,
				MM_DBUS_PROPERTIES_INTERFACE, DBUS_METHOD_GETALL,
				reply_fn, ME_GET_PROPERTIES_FAILED,
				DBUS_TYPE_STRING, &interface,
				DBUS_TYPE_INVALID);
}

static void modem_get_properties_reply(DBusPendingCall *call, void *user_data);
static void modem_get_gsm_properties_reply(DBusPendingCall *call, void *user_data);

/**
 * modem_get_properties_task:
 * @modem: modem object
 *
 * Send a DBUS message to retrieve several properties of a modem
 */
static void modem_get_properties_task(struct modem_data *modem)
{
	modem_get_properties(modem,
			     MM_MODEM_INTERFACE,
			     modem_get_properties_reply);
}

static void modem_get_gsm_network_properties_task(struct modem_data *modem)
{
	modem_get_properties(modem,
			     MM_MODEM_GSM_NETWORK_INTERFACE,
			     modem_get_gsm_properties_reply);
}

static void modem_get_gsm_card_properties_task(struct modem_data *modem)
{
	if (modem->type == MM_MODEM_TYPE_GSM)
		modem_get_properties(modem,
				     MM_MODEM_GSM_CARD_INTERFACE,
				     modem_get_gsm_properties_reply);
	else
		modem_task_run(modem);
}

static connman_bool_t check_property(const char *wanted_key,
				     const char *supplied_key,
				     DBusMessageIter *value,
				     gint target_type,
				     gint *error)
{
	if (wanted_key == NULL || value == NULL || supplied_key == NULL ||
	    error == NULL) {
		if (error != NULL) {
			(*error)++;
		}
		CONNMAN_ERROR("Expected non-null value");
		return FALSE;
	}
	if (*error || g_str_equal(wanted_key, supplied_key) == FALSE) {
		return FALSE;
	}
	gint type = dbus_message_iter_get_arg_type(value);
	if (type != target_type) {
		CONNMAN_ERROR("[%s]: Expected type 0x%x, got 0x%x",
			      wanted_key,
			      target_type,
			      type);
		(*error)++;
		return FALSE;
	}
	return TRUE;
}

static connman_bool_t modem_dbus_extract_uint32(const char *wanted_key,
				      const char *supplied_key,
				      DBusMessageIter *value,
				      dbus_uint32_t *output,
				      gint *error)
{
	if (check_property(wanted_key, supplied_key,
			   value, DBUS_TYPE_UINT32, error)) {
		dbus_message_iter_get_basic(value, output);
		return TRUE;
	}
	return FALSE;
}

static connman_bool_t modem_dbus_extract_uint16(const char *wanted_key,
				      const char *supplied_key,
				      DBusMessageIter *value,
				      dbus_uint16_t *output,
				      gint *error)
{
	if (check_property(wanted_key, supplied_key,
			   value, DBUS_TYPE_UINT16, error)) {
		dbus_message_iter_get_basic(value, output);
		return TRUE;
	}
	return FALSE;
}

/**
 * modem_dbus_extract_string:
 * If the supplied key matches the wanted key, set output to the contents of
 * value
 *
 * @wanted_key:  Key that this call is attempting to extract
 * @supplied_key:  key corresponding to the value
 * @value: DBusMessageIter pointing to received value
 * @output: destination for the result.  If NULL, return silently.
 *    Caller must g_free
 * @error:  pointer to error status.  Return if nonzero
 */

static connman_bool_t modem_dbus_extract_string(const char *wanted_key,
				      const char *supplied_key,
				      DBusMessageIter *value,
				      char **output,
				      gint *error)
{
	if (check_property(wanted_key, supplied_key,
			   value, DBUS_TYPE_STRING, error)) {
		const char *by_reference;
		dbus_message_iter_get_basic(value, &by_reference);
		*output = g_strdup(by_reference);
		if (*output == NULL) {
			CONNMAN_ERROR("Out of memory");
			(*error)++;
		}
		return TRUE;
	}
	return FALSE;
}

typedef gint (*property_handler)(struct modem_data *,
			      const char *,
			      DBusMessageIter *);

static enum connman_network_cellular_technology
	mm_gsm_technology_to_connman_technology();

static gint handle_modem_property(struct modem_data *modem,
				  const char *key,
				  DBusMessageIter *value)
{
	dbus_uint32_t modem_state = MM_MODEM_STATE_UNKNOWN;
	gint extract_err = 0;

	if (modem_dbus_extract_string(
			"Device", key, value, &modem->device,
			&extract_err))
		return extract_err;
	if (modem_dbus_extract_string(
			"MasterDevice", key, value,
			&modem->master_device, &extract_err))
		return extract_err;
	if (modem_dbus_extract_string(
			"Driver", key, value, &modem->driver,
			&extract_err))
		return extract_err;
	if (modem_dbus_extract_uint32(
			"Type", key, value,
			(guint *)&modem->type,
			&extract_err))
		return extract_err;
	if (modem_dbus_extract_uint32(
			"IpMethod", key, value,
			(guint *)&modem->ip_method,
			&extract_err))
		return extract_err;
	if (modem_dbus_extract_string(
			"UnlockRequired", key, value,
			&modem->gsm.lock.unlock_required, &extract_err))
		return extract_err;
	if (modem_dbus_extract_uint32(
			"UnlockRetries", key, value,
			&modem->gsm.lock.retries, &extract_err))
		return extract_err;
	if (modem_dbus_extract_uint32(
			"State", key, value,
			&modem_state, &extract_err)) {
		if (modem_state == MM_MODEM_STATE_CONNECTED) {
			_DBG_MODEMMGR("modem starts out in CONNECTED state");
			modem->connect_needed = TRUE;
		}
		return extract_err;
	}
	return extract_err;
}

static gint handle_cdma_property(struct modem_data *modem,
				 const char *key,
				 DBusMessageIter *value)
{
	gint extract_err = 0;

	modem_dbus_extract_string("MEID", key, value, &modem->meid,
				  &extract_err);
	return extract_err;
}

/**
 * handle_gsm_property handles properties for both the
 * Modem.Gsm.Network and Modem.Gsm.Card interfaces.
 */
static gint handle_gsm_property(struct modem_data *modem,
				const char *key,
				DBusMessageIter *value)
{
	guint technology;
	gint extract_err = 0;
	dbus_uint32_t facility_locks;

	if (modem_dbus_extract_uint32("AccessTechnology", key, value,
				      &technology, &extract_err)) {
		modem->network_technology =
			mm_gsm_technology_to_connman_technology(technology);
	} else if (modem_dbus_extract_uint32("EnabledFacilityLocks", key, value,
				      &facility_locks, &extract_err)) {
		modem->gsm.lock.enabled = (facility_locks & MM_MODEM_GSM_FACILITY_SIM) != 0;
	}
	return extract_err;
}

/**
 * handle_get_properties_reply:
 *
 * Process the reply from the DBUS Get (property) call, using
 * the supplied property handler function to look for a specific
 * set of properties.
 */
static void handle_get_properties_reply(DBusPendingCall *call, void *user_data,
					property_handler prop_handler)
{
	struct modem_data *modem = user_data;
	DBusMessage *reply;
	DBusError error;
	gint extract_err = 0;
	enum modem_event event = ME_GET_PROPERTIES_FAILED;
	DBusMessageIter array, dict;
	gint type;

	_DBG_MODEMMGR("modem %s", modem->dbus_path);

	dbus_error_init(&error);

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		goto done_no_reply;
	}

	if (dbus_set_error_from_message(&error, reply)) {
		CONNMAN_ERROR("DBus Error: %s:%s",
			      error.name, error.message);
		goto done;
	}

	if (dbus_message_iter_init(reply, &array) == FALSE) {
		_DBG_MODEMMGR("Could not iterate on reply %s", modem->dbus_path);
		goto done;
	}
	if ((type = dbus_message_iter_get_arg_type(&array)) != DBUS_TYPE_ARRAY) {
		_DBG_MODEMMGR("Expected array, got %x", type);
		goto done;
	}

	dbus_message_iter_recurse(&array, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY &&
	       extract_err == 0) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);
		_DBG_MODEMMGR("Saw key: %s", key);

		extract_err = prop_handler(modem, key, &value);

		dbus_message_iter_next(&dict);
	}
	if (extract_err == 0) {
		event = ME_GET_PROPERTIES_SUCCEEDED;
	}
done:
	dbus_message_unref(reply);
done_no_reply:
	dbus_error_free(&error);
	modem_handle_event(modem, event);
}

static void modem_get_properties_reply(DBusPendingCall *call, void *user_data)
{
	handle_get_properties_reply(call, user_data, handle_modem_property);
}

void modem_get_gsm_properties_reply(DBusPendingCall *call, void *user_data)
{
	handle_get_properties_reply(call, user_data, handle_gsm_property);
}

void modem_get_cdma_properties_reply(DBusPendingCall *call, void *user_data)
{
	handle_get_properties_reply(call, user_data, handle_cdma_property);
}

static void modem_get_gsm_identifier_reply(DBusPendingCall *call, void *user_data);

static void request_gsm_identifier(struct modem_data *modem, const char *method)
{
	_DBG_MODEMMGR("Requesting GSM ID \"%s\"", method);
	modem_dbus_send_message(modem, MM_MODEM_GSM_CARD_INTERFACE, method,
				modem_get_gsm_identifier_reply,
				ME_GET_PROPERTIES_FAILED,
				DBUS_TYPE_INVALID);
}

gboolean imei_needed(struct modem_data *modem)
{
	return modem->imei == NULL;
}

gboolean imsi_needed(struct modem_data *modem)
{
	return modem->imsi == NULL;
}

gboolean spn_needed(struct modem_data *modem)
{
	return modem->gsm.spn == NULL;
}

gboolean msisdn_needed(struct modem_data *modem)
{
	return TRUE;
}


static struct gsm_id_request *get_next_id_request(struct modem_data *modem)
{
	struct gsm_id_request *id_request;

	++modem->gsm.id_index;
	id_request = &gsm_id_requests[modem->gsm.id_index];
	while (id_request->name != NULL && !id_request->id_needed(modem)) {
		++modem->gsm.id_index;
		++id_request;
	}
	if (id_request->name == NULL)
		return NULL;
	return id_request;
}

/**
 * modem_get_type_identifiers_task:
 * @modem: modem object
 *
 * Send a DBUS message to retrieve the identifiers associated with a
 * modem: MEID for CDMA, IMEI, IMSI, SPN, and MSISDN for GSM. Skip
 * identifiers for which we already have a value (most likely because
 * it was returned by the GetStatus method).
 */
static void modem_get_identifiers_task(struct modem_data *modem)
{
	struct gsm_id_request *id_request;

	switch (modem->type) {
	case MM_MODEM_TYPE_CDMA:
		if (modem->meid != NULL) {
			if (modem->pending_task)
				modem_task_run(modem);
			return;
		}
		modem_get_properties(modem,
				     MM_MODEM_CDMA_INTERFACE,
				     modem_get_cdma_properties_reply);
		break;
	case MM_MODEM_TYPE_GSM:
		modem->gsm.id_index = -1;
		id_request = get_next_id_request(modem);
		if (id_request == NULL) {
			if (modem->pending_task)
				modem_task_run(modem);
			return;
		}

		/* Request the first identifier */
		request_gsm_identifier(modem, id_request->method);
		break;
	default:
		connman_warn("%s:%s() unknown modem type %d",
			     __FILE__, __FUNCTION__, modem->type);
		if (modem->pending_task)
			modem_task_run(modem);
		break;
		return;
	}
}

void modem_get_gsm_identifier_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;
	DBusMessage *reply;
	DBusError error;
	char *identifier = NULL;
	struct gsm_id_request *id_request;

	_DBG_MODEMMGR("modem %s", modem->dbus_path);

	dbus_error_init(&error);

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		modem_task_run(modem);
		return;
	}
	id_request = &gsm_id_requests[modem->gsm.id_index];
	if (id_request->name == NULL) {
		_DBG_MODEMMGR("no outstanding identifier request");
		dbus_message_unref(reply);
		modem_task_run(modem);
		return;
	}

	if (dbus_set_error_from_message(&error, reply))
		goto next_id;

	if (dbus_message_get_args(reply, &error,
				  DBUS_TYPE_STRING,
				  &identifier,
				  DBUS_TYPE_INVALID) == FALSE) {
		if (dbus_error_is_set(&error) == TRUE)
			CONNMAN_ERROR("Error retrieving reply argument: %s:%s",
				      error.name, error.message);
		else
			CONNMAN_ERROR("Reply argument type is not STRING");
		goto next_id;
	}

	if (identifier == NULL || strlen(identifier) == 0) {
		_DBG_MODEMMGR("Request for GSM id %s returned null or empty string",
			     id_request->name);
	} else if (g_str_equal(id_request->name, "imei")) {
		g_free(modem->imei);
		modem->imei = g_strdup(identifier);
		connman_device_set_string(modem->cm_device, "Cellular.IMEI",
					  modem->imei);
	} else if (g_str_equal(id_request->name, "imsi")) {
		g_free(modem->imsi);
		modem->imsi = g_strdup(identifier);
		connman_device_set_string(modem->cm_device,
					  "Cellular.IMSI",
					  modem->imsi);
	} else if (g_str_equal(id_request->name, "spn")) {
		g_free(modem->gsm.spn);
		modem->gsm.spn = g_strdup(identifier);
	} else if (g_str_equal(id_request->name, "msisdn")) {
		connman_device_set_string(modem->cm_device, "Cellular.MDN",
					  identifier);
	} else {
		connman_warn("%s:%s() Unhandled identifier request: %s",
			     __FILE__, __FUNCTION__, id_request->name);
	}
next_id:
	dbus_error_free(&error);
	dbus_message_unref(reply);
	id_request = get_next_id_request(modem);
	if (id_request != NULL)
		request_gsm_identifier(modem, id_request->method);
	else {
		set_gsm_provider(modem, modem->cm_device);
		modem_task_run(modem);
	}
}

static void modem_get_status_reply(DBusPendingCall *call, void *user_data);

/**
 * modem_get_status_task:
 * @modem: modem object
 *
 * Send a DBUS message to retrieve the status of a modem in the
 * form of a dictionary.
 */
static void modem_get_status_task(struct modem_data *modem)
{
	modem_dbus_send_message(modem,
				MM_MODEM_SIMPLE_INTERFACE,
				MM_MODEM_SIMPLE_METHOD_GETSTATUS,
				modem_get_status_reply,
				ME_GET_PROPERTIES_FAILED,
				DBUS_TYPE_INVALID);
}

static gint handle_status_property(struct modem_data *modem,
				   const char *key,
				   DBusMessageIter *value)
{
	dbus_uint32_t modem_state = MM_MODEM_STATE_UNKNOWN;
	dbus_uint32_t mm_activation_state =
			MM_MODEM_CDMA_ACTIVATION_STATE_NOT_ACTIVATED;
	char *carrier = NULL;
	char *meid = NULL;
	char *imei = NULL;
	char *imsi = NULL;
	char *esn = NULL;
	char *min = NULL;
	char *mdn = NULL;
	char *firmware_revision = NULL;
	char *olp = NULL;
	char *usage_url = NULL;
	dbus_uint16_t prl_version = 0;
	gint extract_err = 0;

	if (modem_dbus_extract_string(
			"carrier", key, value, &carrier,
			&extract_err)) {
		modem->configured_carrier = g_strdup(carrier);
		connman_device_set_string(modem->cm_device,
					  "Cellular.Carrier",
					  modem->configured_carrier);
		if (modem->type == MM_MODEM_TYPE_CDMA) {
                        /*
                         * See previous about wondering what to use as
                         * the value for the home provider country for
                         * CDMA.
                         */
			struct connman_network_operator op = {
				.name = modem->configured_carrier,
				.code = "",
				.country = "us"};
			connman_device_set_home_provider(modem->cm_device, &op);
			connman_device_set_string(modem->cm_device,
				"Cellular.FirmwareImageName",
				modem->configured_carrier);
		}
	} else if (modem_dbus_extract_string(
			"meid", key, value, &meid,
			&extract_err)) {
		g_free(modem->meid);
		modem->meid = g_strdup(meid);
		connman_device_set_string(modem->cm_device,
					  "Cellular.MEID",
					  modem->meid);
	} else if (modem_dbus_extract_string(
			"imei", key, value, &imei,
			&extract_err)) {
		g_free(modem->imei);
		modem->imei = g_strdup(imei);
		connman_device_set_string(modem->cm_device,
					  "Cellular.IMEI",
					  modem->imei);
	} else if (modem_dbus_extract_string(
			"imsi", key, value, &imsi,
			&extract_err)) {
		g_free(modem->imsi);
		modem->imsi = g_strdup(imsi);
		connman_device_set_string(modem->cm_device,
					  "Cellular.IMSI",
					  modem->imsi);
		if (modem->type == MM_MODEM_TYPE_GSM)
                        set_gsm_provider(modem, modem->cm_device);
	} else if (modem_dbus_extract_string(
			"esn", key, value, &esn,
			&extract_err)) {
		connman_device_set_string(modem->cm_device,
					  "Cellular.ESN", esn);
	} else if (modem_dbus_extract_string(
			"mdn", key, value, &mdn,
			&extract_err)) {
		connman_device_set_string(modem->cm_device,
					  "Cellular.MDN", mdn);
	} else if (modem_dbus_extract_string(
			"min", key, value, &min,
			&extract_err)) {
		connman_device_set_string(modem->cm_device,
					  "Cellular.MIN", min);
	} else if (modem_dbus_extract_string(
			"payment_url", key, value, &olp,
			&extract_err)) {
		g_free(modem->olp.url);
		modem->olp.url = g_strdup(olp);
	} else if (modem_dbus_extract_string(
			"payment_url_method", key, value, &olp,
			&extract_err)) {
		g_free(modem->olp.method);
		modem->olp.method = g_strdup(olp);
	} else if (modem_dbus_extract_string(
			"payment_url_postdata", key, value, &olp,
			&extract_err)) {
		g_free(modem->olp.postdata);
		modem->olp.postdata = g_strdup(olp);
	} else if (modem_dbus_extract_string(
			"usage_url", key, value, &usage_url,
			&extract_err)) {
		g_free(modem->usage_url);
		modem->usage_url = g_strdup(usage_url);
	} else if (modem_dbus_extract_uint16(
			"prl_version", key, value, &prl_version,
			&extract_err)) {
		connman_device_set_prl_version(modem->cm_device,
					       prl_version);
	} else if (modem_dbus_extract_uint32(
			"state", key, value, &modem_state,
			&extract_err)) {
		if (modem_state == MM_MODEM_STATE_CONNECTED) {
			_DBG_MODEMMGR("modem starts out in CONNECTED state");
			modem->connect_needed = TRUE;
		}
	} else if (modem_dbus_extract_uint32(
			"activation_state", key, value,
			&mm_activation_state, &extract_err)) {
		modem->cdma.activation_state =
				convert_activation_state(mm_activation_state);
	} else if (modem_dbus_extract_string(
			"firmware_revision", key, value, &firmware_revision,
			&extract_err)) {
		connman_device_set_string(modem->cm_device,
					  "Cellular.FirmwareRevision",
					  firmware_revision);
	}
	return extract_err;
}

/**
 * modem_get_status_reply:
 * @modem: modem object
 *
 * Process the reply from the DBUS GetStatus call
 */
static void modem_get_status_reply(DBusPendingCall *call, void *user_data)
{
	handle_get_properties_reply(call, user_data, handle_status_property);
}

static void modem_get_info_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;
	DBusMessage *reply;
	DBusError error;
	char *manufacturer;
	char *model_id;
	char *hardware_revision;
	DBusMessageIter structure, field;
	enum modem_event event = ME_GETINFO_FAILED;

	_DBG_MODEMMGR("modem %s", modem->dbus_path);

	dbus_error_init(&error);

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		goto done_no_reply;
	}

	if (dbus_set_error_from_message(&error, reply)) {
		CONNMAN_ERROR("DBus Error: %s:%s",
			      error.name, error.message);
		goto done;
	}

	if (dbus_message_iter_init(reply, &structure) == FALSE) {
		_DBG_MODEMMGR("Could not iterate on reply %s", modem->dbus_path);
		goto done;
	}
	gint type;
	if ((type = dbus_message_iter_get_arg_type(&structure)) != DBUS_TYPE_STRUCT) {
		_DBG_MODEMMGR("Expected struct, got %x", type);
		goto done;
	}

	dbus_message_iter_recurse(&structure, &field);

	if (dbus_message_iter_get_arg_type(&field) != DBUS_TYPE_STRING)
		goto done;
	dbus_message_iter_get_basic(&field, &manufacturer);
	dbus_message_iter_next(&field);
	if (dbus_message_iter_get_arg_type(&field) != DBUS_TYPE_STRING)
		goto done;
	dbus_message_iter_get_basic(&field, &model_id);
	dbus_message_iter_next(&field);
	if (dbus_message_iter_get_arg_type(&field) != DBUS_TYPE_STRING)
		goto done;
	dbus_message_iter_get_basic(&field, &hardware_revision);
	dbus_message_iter_next(&field);

	event = ME_GETINFO_SUCCEEDED;

	connman_device_set_string(modem->cm_device,
				  "Cellular.Manufacturer",
				  manufacturer);
	connman_device_set_string(modem->cm_device,
				  "Cellular.ModelID",
				  model_id);
	connman_device_set_string(modem->cm_device,
				  "Cellular.HardwareRevision",
				  hardware_revision);

done:
	dbus_error_free(&error);
	dbus_message_unref(reply);
done_no_reply:
	modem_handle_event(modem, event);
}

/**
 * modem_get_info_task:
 * @modem: modem object
 *
 * Send a DBUS message to retrieve the information about a modem.
 * Returns three strings: manufacturer, model ID, and hardware revision.
 */
static void modem_get_info_task(struct modem_data *modem)
{
	modem_dbus_send_message(modem,
				MM_MODEM_INTERFACE,
				MM_MODEM_METHOD_GETINFO,
				modem_get_info_reply,
				ME_GETINFO_FAILED,
				DBUS_TYPE_INVALID);
}

static struct modem_method_callback *modem_create_method_callback(
		struct modem_data *modem,
		void *callback_data,
		error_conversion_func error_func)
{
	struct modem_method_callback *cb;

	cb = g_new0(struct modem_method_callback, 1);
	cb->modem = modem;
	cb->cb_data = callback_data;
	cb->convert_error = error_func;
	return cb;
}

static int modem_device_require_pin(struct connman_device *device,
			    const char *pin, connman_bool_t require,
			    struct connman_dbus_method_callback *callback)
{
	struct modem_method_callback *cb;
	struct modem_data *modem = modem_device_find_modem(device, __func__);
	int err;

	if (modem == NULL) {
		return -ENODEV;
	}
	cb = modem_create_method_callback(modem, callback, convert_error);
	err = modem_dbus_send_message_with_pending(
			modem, cb,
			DEFAULT_TIMEOUT_MS,
			MM_MODEM_GSM_CARD_INTERFACE,
			MM_MODEM_GSM_CARD_METHOD_ENABLEPIN,
			DBUS_TYPE_STRING, &pin,
			DBUS_TYPE_BOOLEAN, &require,
			DBUS_TYPE_INVALID);
	if (err < 0)
		g_free(cb);
	return err;
}

static int modem_device_enter_pin(struct connman_device *device,
				  const char *pin,
				  struct connman_dbus_method_callback *callback)
{
	struct modem_method_callback *cb;
	struct modem_data *modem = modem_device_find_modem(device, __func__);
	int err;

	if (modem == NULL) {
		return -ENODEV;
	}
	cb = modem_create_method_callback(modem, callback, convert_error);
	err = modem_dbus_send_message_with_pending(
			modem, cb,
			DEFAULT_TIMEOUT_MS,
			MM_MODEM_GSM_CARD_INTERFACE,
			MM_MODEM_GSM_CARD_METHOD_SENDPIN,
			DBUS_TYPE_STRING, &pin,
			DBUS_TYPE_INVALID);
	if (err < 0)
		g_free(cb);
	return err;
}

static int modem_device_unblock_pin(struct connman_device *device,
				    const char *unblock_code, const char *pin,
				    struct connman_dbus_method_callback *callback)
{
	struct modem_method_callback *cb;
	struct modem_data *modem = modem_device_find_modem(device, __func__);
	int err;

	if (modem == NULL) {
		return -ENODEV;
	}
	cb = modem_create_method_callback(modem, callback, convert_error);
	err = modem_dbus_send_message_with_pending(
			modem, cb,
			DEFAULT_TIMEOUT_MS,
			MM_MODEM_GSM_CARD_INTERFACE,
			MM_MODEM_GSM_CARD_METHOD_SENDPUK,
			DBUS_TYPE_STRING, &unblock_code,
			DBUS_TYPE_STRING, &pin,
			DBUS_TYPE_INVALID);
	if (err < 0)
		g_free(cb);
	return err;
}

static int modem_device_change_pin(struct connman_device *device,
				   const char *old_pin, const char *new_pin,
				   struct connman_dbus_method_callback *callback)
{
	struct modem_method_callback *cb;
	struct modem_data *modem = modem_device_find_modem(device, __func__);
	int err;

	if (modem == NULL) {
		return -ENODEV;
	}
	cb = modem_create_method_callback(modem, callback, convert_error);
	err = modem_dbus_send_message_with_pending(
			modem, cb,
			DEFAULT_TIMEOUT_MS,
			MM_MODEM_GSM_CARD_INTERFACE,
			MM_MODEM_GSM_CARD_METHOD_CHANGEPIN,
			DBUS_TYPE_STRING, &old_pin,
			DBUS_TYPE_STRING, &new_pin,
			DBUS_TYPE_INVALID);
	if (err < 0)
		g_free(cb);
	return err;
}

static const char *ThreeGppCodeToTechString[] = {
	"GSM", "GSM Compact", "UMTS", "EDGE", "HSDPA", "HSUPA", "HSPA"
};
static const int kNumTechStrings =
	sizeof(ThreeGppCodeToTechString) / sizeof(const char *);

static void handle_scan_reply(DBusPendingCall *call, void *user_data)
{
	struct modem_data *modem = user_data;
	DBusMessage *reply;
	DBusError error;
	DBusMessageIter array, subarray;
	gint type;
	GPtrArray *networks;

	if (modem->cm_device == NULL)
		return;

	reply = modem_dbus_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		return;
	}

	/*
	 * From here on, the UI will be notified of something, even if it's
	 * an empty list.
	 */
	networks = g_ptr_array_sized_new(4);

	dbus_error_init(&error);
	if (dbus_set_error_from_message(&error, reply)) {
		CONNMAN_ERROR("Error reply: %s:%s",
			      error.name, error.message);
		dbus_error_free(&error);
		goto out;
	}
	dbus_error_free(&error);

	if (dbus_message_iter_init(reply, &array) == FALSE) {
		CONNMAN_ERROR("Could not iterate on reply %s", modem->dbus_path);
		goto out;
	}
	if ((type = dbus_message_iter_get_arg_type(&array)) != DBUS_TYPE_ARRAY) {
		CONNMAN_ERROR("Expected array, got %x", type);
		goto out;
	}

	g_ptr_array_set_free_func(networks, free_dict);
	dbus_message_iter_recurse(&array, &subarray);

	while (dbus_message_iter_get_arg_type(&subarray) == DBUS_TYPE_ARRAY) {
		char *status = NULL;
		char *network_id = NULL;
		const char *long_name = NULL;
		char *short_name = NULL;
		char *access_tech = NULL;
		DBusMessageIter dict;
		GHashTable *props;
		props =	g_hash_table_new_full(g_str_hash, g_str_equal,
					      NULL, g_free);

		dbus_message_iter_recurse(&subarray, &dict);
		while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY) {
			DBusMessageIter entry;
			const char *key;

			dbus_message_iter_recurse(&dict, &entry);
			dbus_message_iter_get_basic(&entry, &key);
			dbus_message_iter_next(&entry);
			if ((type = dbus_message_iter_get_arg_type(&entry)) != DBUS_TYPE_STRING) {
				CONNMAN_ERROR("Expected string value with key"
					      "'%s', got %x", key, type);
				goto out;
			}

			if (g_str_equal(key, "status") == TRUE)
				dbus_message_iter_get_basic(&entry, &status);
			else if (g_str_equal(key, "operator-num"))
				dbus_message_iter_get_basic(&entry, &network_id);
			else if (g_str_equal(key, "operator-long"))
				dbus_message_iter_get_basic(&entry, &long_name);
			else if (g_str_equal(key, "operator-short"))
				dbus_message_iter_get_basic(&entry, &short_name);
			else if (g_str_equal(key, "access-tech"))
				dbus_message_iter_get_basic(&entry, &access_tech);
			else
				connman_warn("%s: unknown scan list property %s",
					     __func__, key);
			dbus_message_iter_next(&dict);
		}
		dbus_message_iter_next(&subarray);

		if (long_name == NULL && network_id != NULL) {
			struct mobile_provider *provider =
				mobile_provider_lookup_by_network(provider_db,
								  network_id);
			if (provider != NULL)
				long_name = mobile_provider_get_name(provider);
		}
		/*
		 * The numerical values for 'status' and 'access-tech'
		 * are from 3GPP TS 27.007 Section 7.3
		 */
		if (status != NULL) {
			if (g_str_equal(status, "0"))
				status = "unknown";
			else if (g_str_equal(status, "1"))
				status = "available";
			else if (g_str_equal(status, "2"))
				status = "current";
			else if (g_str_equal(status, "3"))
				status = "forbidden";
			g_hash_table_insert(props, "status", g_strdup(status));
		}
		if (network_id != NULL)
			g_hash_table_insert(props, "network_id",
					    g_strdup(network_id));
		if (short_name != NULL)
			g_hash_table_insert(props, "short_name",
					    g_strdup(short_name));
		if (long_name != NULL)
			g_hash_table_insert(props, "long_name",
					    g_strdup(long_name));
		if (access_tech != NULL) {
			const char *techstr = NULL;
			char *endp;
			errno = 0;
			long code = strtol(access_tech, &endp, 0);
			if (errno != ERANGE && endp != access_tech &&
			    code >= 0 && code < kNumTechStrings)
				techstr = ThreeGppCodeToTechString[code];

			if (techstr != NULL)
				g_hash_table_insert(props, "technology",
						    g_strdup(techstr));
		}
		g_ptr_array_add(networks, props);
	}
out:
	dbus_message_unref(reply);
	connman_device_set_found_networks(modem->cm_device, networks);
}

static int modem_device_scan(struct connman_device *device)
{
	struct modem_data *modem = modem_device_find_modem(device, __func__);

	if (modem == NULL)
		return -ENODEV;
	_DBG_MODEMMGR("modem %s", modem->dbus_path);
	if (modem->type != MM_MODEM_TYPE_GSM)
		return -EOPNOTSUPP;
	return modem_dbus_send_message_with_timeout(modem,
				       MM_MODEM_GSM_NETWORK_INTERFACE,
				       MM_MODEM_GSM_NETWORK_METHOD_SCAN,
				       handle_scan_reply,
				       NETWORK_SCAN_TIMEOUT_MS,
				       ME_NO_EVENT,
				       DBUS_TYPE_INVALID);
}

static int modem_device_register(struct connman_device *device,
				 const char *network_id,
				 struct connman_dbus_method_callback *callback)
{
	struct modem_data *modem = modem_device_find_modem(device, __func__);
	struct modem_method_callback *cb;

	if (modem == NULL)
		return -ENODEV;
	if (modem->type != MM_MODEM_TYPE_GSM)
		return -EOPNOTSUPP;
	_DBG_MODEMMGR("modem %s register on %s", modem->dbus_path,
		      network_id && *network_id ? network_id : "home network");
	cb = modem_create_method_callback(modem, callback, convert_register_error);
	return modem_dbus_send_message_with_pending(modem,
					cb,
					NETWORK_REGISTER_TIMEOUT_MS,
					MM_MODEM_GSM_NETWORK_INTERFACE,
					MM_MODEM_GSM_NETWORK_METHOD_REGISTER,
					DBUS_TYPE_STRING,
					&network_id,
					DBUS_TYPE_INVALID);

}

static enum connman_network_cellular_roaming_state mm_cdma_roaming_to_connman_roaming(
		guint mm_roaming_state)
{
	switch (mm_roaming_state) {
	case MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN:
	case MM_MODEM_CDMA_REGISTRATION_STATE_REGISTERED:
		return CONNMAN_NETWORK_ROAMING_STATE_UNKNOWN;
	case MM_MODEM_CDMA_REGISTRATION_STATE_HOME:
		return CONNMAN_NETWORK_ROAMING_STATE_HOME;
	case MM_MODEM_CDMA_REGISTRATION_STATE_ROAMING:
		return CONNMAN_NETWORK_ROAMING_STATE_ROAMING;
	}
	return CONNMAN_NETWORK_ROAMING_STATE_UNKNOWN;
}

static enum connman_network_cellular_roaming_state mm_gsm_regstate_to_connman_roaming(
		guint mm_gsm_regstate)
{
	switch (mm_gsm_regstate) {
	case MM_MODEM_GSM_NETWORK_REG_STATUS_HOME:
		return CONNMAN_NETWORK_ROAMING_STATE_HOME;
	case MM_MODEM_GSM_NETWORK_REG_STATUS_ROAMING:
		return CONNMAN_NETWORK_ROAMING_STATE_ROAMING;
	default:
		return CONNMAN_NETWORK_ROAMING_STATE_UNKNOWN;
	}
}

static enum connman_network_cellular_technology mm_gsm_technology_to_connman_technology(
		guint mm_technology) {
	switch (mm_technology) {
	case MM_MODEM_GSM_ACCESS_TECH_GSM:
	case MM_MODEM_GSM_ACCESS_TECH_GSM_COMPACT:
		return CONNMAN_NETWORK_TECHNOLOGY_GSM;
		break;
	case MM_MODEM_GSM_ACCESS_TECH_GPRS:
		return CONNMAN_NETWORK_TECHNOLOGY_GPRS;
		break;
	case MM_MODEM_GSM_ACCESS_TECH_EDGE:
		return CONNMAN_NETWORK_TECHNOLOGY_EDGE;
		break;
	case MM_MODEM_GSM_ACCESS_TECH_UMTS:
		return CONNMAN_NETWORK_TECHNOLOGY_UMTS;
		break;
	case MM_MODEM_GSM_ACCESS_TECH_HSDPA:
	case MM_MODEM_GSM_ACCESS_TECH_HSUPA:
	case MM_MODEM_GSM_ACCESS_TECH_HSPA:
		return CONNMAN_NETWORK_TECHNOLOGY_HSPA;
		break;
	case MM_MODEM_GSM_ACCESS_TECH_HSPA_PLUS:
		return CONNMAN_NETWORK_TECHNOLOGY_HSPA_PLUS;
		break;
	}
	return CONNMAN_NETWORK_TECHNOLOGY_UNKNOWN;
}

/**
 * Return TRUE if the modem is registered on a network
 */
static int
modem_is_registered(struct modem_data *modem) {
	return (modem->cdma._1x_registration_state !=
			MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN ||
		modem->cdma.evdo_registration_state !=
			MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN ||
		modem->gsm.registration_state ==
			MM_MODEM_GSM_NETWORK_REG_STATUS_HOME ||
		modem->gsm.registration_state ==
			 MM_MODEM_GSM_NETWORK_REG_STATUS_ROAMING
		);
}

static void
modem_clear_registration_state(struct modem_data *modem) {
	modem->cdma.evdo_registration_state =
		MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN;
	modem->cdma._1x_registration_state =
		MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN;
	modem->gsm.registration_state =
		MM_MODEM_GSM_NETWORK_REG_STATUS_IDLE;
}

/**
 * Handle a change in the registration state. If the device was
 * not registered but now is, create a connman_network and a
 * connman_service. This causes the mobile data network to
 * become visible to the UI.
 */
static int modem_registration_state_change(struct modem_data *modem)
{
	struct connman_network *network = get_network(modem);

	_DBG_MODEMMGR("network %p", network);

	if (modem_is_registered(modem)) {
		if (modem->state == MODEM_STATE_ENABLED) {
			/*
			 * Once connected the modem could change the type of
			 * registration and still stay connected.
			 */
			modem->state = MODEM_STATE_REGISTERED;
		}
		if (network == NULL) {
			network = create_network(modem);
		}
		if (network == NULL) {
			return -ENODEV;
		}
		if (modem->cdma.evdo_registration_state !=
		    MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN) {
			connman_network_set_string(network,
						   "Network Type",
						   "CDMA: EVDO");
			connman_network_set_registration_info(
					network,
					CONNMAN_NETWORK_TECHNOLOGY_EVDO,
					mm_cdma_roaming_to_connman_roaming(
						modem->cdma.evdo_registration_state));


		} else if (modem->cdma._1x_registration_state !=
			   MM_MODEM_CDMA_REGISTRATION_STATE_UNKNOWN) {
			connman_network_set_string(network,
						   "Network Type",
						   "CDMA: 1x");
			connman_network_set_registration_info(
					network,
					CONNMAN_NETWORK_TECHNOLOGY_1XRTT,
					mm_cdma_roaming_to_connman_roaming(
						modem->cdma._1x_registration_state));
		} else {
			connman_network_set_string(network,
						   "Network Type",
						   "GSM");
			connman_network_set_registration_info(
					network,
					modem->network_technology,
					mm_gsm_regstate_to_connman_roaming(
						modem->gsm.registration_state));
			if (modem->state == MODEM_STATE_CONNECTED &&
			    modem->gsm.registration_state ==
				    MM_MODEM_GSM_NETWORK_REG_STATUS_ROAMING &&
			    !connman_device_roaming_allowed(modem->cm_device)) {
				connman_error("%s: disconnecting: "
					      "roaming connections not allowed",
					      __func__);
				return modem_task_run_new(modem,
							  &disconnect_modem_work);
				return 0;
			}
		}
		connman_network_set_available(network, TRUE);
		connman_network_set_scangen(network, ++modem->scangen);
		connman_network_set_strength(network, modem->signal_strength);
		modem_get_signal_quality(modem);
	} else {
		if (network != NULL) {
			_DBG_MODEMMGR("Removing network %s",
				      modem->network_dbus_name);

			connman_device_remove_network(modem->cm_device,
						      modem->network_dbus_name);
		}
		if (modem->state == MODEM_STATE_CONNECTED ||
		    modem->state == MODEM_STATE_REGISTERED) {
			modem->state = MODEM_STATE_ENABLED;
		}
	}
	return 0;
}


/**
 * modem_set_connected
 *
 * Handle a change in the connected state of the modem.  Once the
 * modem is connected, the network should become active, and if
 * necessary, flimflam should start dhcp or ppp.
 */
static int modem_set_connected(struct modem_data *modem, gboolean connected)
{
	struct connman_network *network = get_network(modem);
	int index = connman_inet_ifindex(modem->device);

	_DBG_MODEMMGR("connected: %d", connected);
	if (network == NULL) {
		_DBG_MODEMMGR("No network");
		switch(modem->state) {
		case MODEM_STATE_REGISTERED:
		case MODEM_STATE_CONNECTED:
			modem->state = MODEM_STATE_ENABLED;
			break;
		default:
			break;
		}
		return -ENODEV;
	}

	if (connected) {
		/* for modems using ppp index will be -1 */
		if (index != -1)
			connman_inet_ifup(index);
		modem->state = MODEM_STATE_CONNECTED;
		connman_device_set_connected(modem->cm_device, TRUE);
		connman_network_set_connected(network);
		modem_record_connect_info(modem);
	} else {
		if (modem->state == MODEM_STATE_CONNECTED) {
			modem->state = MODEM_STATE_REGISTERED;
		}
		connman_network_set_disconnected(network);
		/* for modems using ppp index will be -1 */
		if (index != -1)
			connman_inet_ifdown(index);
	}
	return 0;
}

static connman_bool_t can_retry_connect(struct modem_data *modem)
{
	return modem->type == MM_MODEM_TYPE_GSM &&
			modem->gsm.connect_tries < MAX_CONNECT_TRIES;
}

static void set_next_apn(struct modem_data *modem, char *apn, char *username,
		     char *password)
{
	struct connman_network_apn *next_apn = &modem->gsm.next_apn;
	g_free(next_apn->apn);
	g_free(next_apn->network_id);
	g_free(next_apn->username);
	g_free(next_apn->password);
	next_apn->apn = g_strdup(apn);
	next_apn->username = g_strdup(username);
	next_apn->password = g_strdup(password);
	if (apn == NULL)
		next_apn->network_id = NULL;
	else
		next_apn->network_id = g_strdup(modem->gsm.network_id);
}

static void next_apn_try_state(struct modem_data *modem)
{
	struct connman_network *network;
	const struct mobile_apn *apn;
	const struct connman_network_apn *user_apn = NULL;
	const struct connman_network_apn *saved_apn = NULL;

	_DBG_MODEMMGR("APN try state %d", modem->gsm.apn_try_state);
	network = connman_device_get_network(modem->cm_device,
					     modem->network_dbus_name);
	switch (modem->gsm.apn_try_state) {
	case APN_TRY_START:
		modem->gsm.apn_try_state = APN_TRY_SAVED;
		saved_apn = connman_network_get_last_good_apn(network);
		if (saved_apn != NULL) {
			_DBG_MODEMMGR("Trying last good APN %s",
				      saved_apn->apn);
			set_next_apn(modem, saved_apn->apn, saved_apn->username,
				 saved_apn->password);
			break;
		}
		/* ... fall through ... */
	case APN_TRY_SAVED:
		modem->gsm.apn_try_state = APN_TRY_USER_APN;
		if (network != NULL)
			user_apn = connman_network_get_apn(network);
		if (user_apn != NULL) {
			_DBG_MODEMMGR("Trying user-supplied APN %s",
				      user_apn->apn);
			set_next_apn(modem, user_apn->apn, user_apn->username,
				 user_apn->password);
			break;
		}
		/* ... fall through ... */
	case APN_TRY_USER_APN:
		modem->gsm.apn_try_state = APN_TRY_LIST;
		modem->gsm.apn_index = 0;
		/* ... fall through ... */
	case APN_TRY_LIST:
		if (modem->gsm.provider != NULL &&
		    modem->gsm.apn_index < modem->gsm.provider->num_apns) {
			apn = modem->gsm.provider->apns[modem->gsm.apn_index++];
			set_next_apn(modem, apn->value, apn->username, apn->password);
			_DBG_MODEMMGR("From APN list for %s: %s",
				      mobile_provider_get_name(modem->gsm.provider),
				      apn->value);
			break;
		}
		modem->gsm.apn_try_state = APN_TRY_NULL_APN;
		_DBG_MODEMMGR("Trying null APN");
		set_next_apn(modem, NULL, NULL, NULL);
		break;
	case APN_TRY_NULL_APN:
		_DBG_MODEMMGR("No more APNs to try, failing");
		modem->gsm.apn_try_state = APN_TRY_FAILED;
		break;
	case APN_TRY_FAILED:
		/* should never get here */
		connman_warn("%s: tried to advance beyond APN_TRY_FAILED state",
			     __func__);
		break;
	}
}

static void reset_connect_state(struct modem_data *modem)
{
	if (modem->type == MM_MODEM_TYPE_GSM) {
		modem->gsm.connect_tries = 0;
		modem->gsm.apn_try_state = APN_TRY_START;
		next_apn_try_state(modem);
	}
}

/**
 * modem_connect_failed
 *
 * Handle a failure to connect to the modem
 */
static int modem_connect_failed(struct modem_data *modem, connman_bool_t retry)
{
	struct connman_network *network = get_network(modem);

	_DBG_MODEMMGR("retry=%d tries so far=%d", retry,
		      modem->gsm.connect_tries);
	if (network != NULL) {
		connman_network_save_last_good_apn(network, NULL);
		if (retry && can_retry_connect(modem)) {
			next_apn_try_state(modem);
			if (modem->gsm.apn_try_state != APN_TRY_FAILED) {
				/*
				 * Make sure the top-level connect timer gets
				 * reset every time we retry a connection
				 * attempt, e.g., with a new APN.
				 */
				connman_network_restart_connect_timer(network);
				modem_connect_start(modem);
				return 0;
			}
		}
	}
	modem_set_connected(modem, FALSE);
	if (network == NULL) {
		_DBG_MODEMMGR("No network");
		return -ENODEV;
	}
	connman_network_set_error(network,
				  CONNMAN_ELEMENT_ERROR_CONNECT_FAILED);
	return 0;
}


/**
 * modem_set_powered
 *
 * Handle a change in the powered state of the modem.
 */
static void modem_set_powered(struct modem_data *modem, gboolean powered)
{
	connman_device_set_powered(modem->cm_device, powered);
}

/*
 * Indicate an enable or disable failure.
 */
static void modem_set_powered_failed(struct modem_data *modem,
				     enum connman_element_error error)
{
	connman_device_set_powered_failed(modem->cm_device, error);
}

/**
 * Handle a change in the signal quality
 */
static void modem_update_signal_quality(struct modem_data *modem)
{
	struct connman_network *network = get_network(modem);

	if (network != NULL) {
		connman_network_set_scangen(network, ++modem->scangen);
		connman_network_set_strength(network, modem->signal_strength);
	}
}

/**
 * Implementation of the modem state machine
 *
 */
static void modem_handle_event(struct modem_data *modem,
			       enum modem_event event)
{
	const char *event_name = eventToString(event);
	const char *state_name = stateToString(modem->state);
	_DBG_MODEMMGR("modem %p state %s event %s", modem, state_name, event_name);

	switch (modem->state) {
	case MODEM_STATE_UNINITIALIZED:
		switch (event) {
		case ME_ENABLE_SUCCEEDED:
		case ME_DISABLE_SUCCEEDED:
		case ME_GETINFO_SUCCEEDED:
		case ME_GET_PROPERTIES_SUCCEEDED:
			modem_task_run(modem);
			return;
		case ME_ENABLE_FAILED:
		case ME_DISABLE_FAILED:
		case ME_GETINFO_FAILED:
		case ME_GET_PROPERTIES_FAILED:
		case ME_CREATE_DEVICE_FAILED:
			modem->state = MODEM_STATE_FAILED;
			modem_task_completed(&modem);
			return;
		default:
			break;
		}
		_DBG_MODEMMGR("Invalid event %s for state UNINITIALIZED",
			      event_name);
		return;

	case MODEM_STATE_DISABLED:
		switch (event) {
		case ME_REGISTRATION_STATE_CHANGED:
			if (modem->pending_task == FALSE) {
				/* N.B. may transition to REGISTERED */
				modem_registration_state_change(modem);
			}
			return;
		case ME_ENABLE_SUCCEEDED:
			modem->state = MODEM_STATE_ENABLED;
			modem_task_run(modem);
			return;
		case ME_ENABLE_FAILED:
			modem_task_completed(&modem);
			return;
		case ME_CREATE_DEVICE_FAILED:
			modem->state = MODEM_STATE_FAILED;
			return;
		default:
			break;
		}
		_DBG_MODEMMGR("Invalid event %s for state %s\n",
			      event_name, state_name);
		return;
	case MODEM_STATE_ENABLED:
		switch (event) {
		case ME_REGISTRATION_STATE_CHANGED:
			/*
			 * If a task is pending, then we are really in
			 * the process of enabling the modem.
			 * Suppress the transition to REGISTERED until
			 * the enabling task has completed.  This
			 * prevents connman from trying to autoconnect
			 * before we have finished enabling.
			 */
			if (modem->pending_task == FALSE) {
				/* N.B. may transition to REGISTERED */
				modem_registration_state_change(modem);
			}
			return;
		case ME_DISABLE_SUCCEEDED:
			modem_set_powered(modem, FALSE);
			modem->state = MODEM_STATE_DISABLED;
			modem_task_run(modem);
			return;
		case ME_DISABLE_FAILED:
			modem_set_powered(modem, TRUE);
			modem_task_completed(&modem);
			return;
		case ME_REGISTER_SUCCEEDED:
			/*
			 * Do not change state yet,
			 * wait for ME_GET_REGISTRATION_STATE_SUCCEEDED
			 */
			modem_task_run(modem);
			return;
		case ME_REGISTER_FAILED:
			/* Clear the selected network, if any. If there was
			 * a selected network, try to register on the home
			 * network. */
			if (connman_device_get_selected_network(modem->cm_device)) {
				connman_device_clear_selected_network(modem->cm_device);
				modem_task_rerun(modem);
				return;
			}
			modem->gsm.registration_state =
				MM_MODEM_GSM_NETWORK_REG_STATUS_IDLE;
			modem_registration_state_change(modem);
			/* run the rest of the tasks even if not registered */
			modem_task_run(modem);
			return;
		case ME_GET_REGISTRATION_STATE_SUCCEEDED:
			/*
			 * Complete the enable task first.  Then
			 * process the registration change, which will
			 * create a connman network object and may
			 * auto connect.
			 */
			modem_task_run(modem);  /* process modem_report_enabled_task */
			/* N.B. may transition to REGISTERED */
			if (modem != NULL)
				modem_registration_state_change(modem);
			return;
		case ME_GET_REGISTRATION_STATE_FAILED:
			modem_task_completed(&modem);
			return;
		case ME_GET_SIGNALQUALITY_FAILED:
			return;
		case ME_GET_SIGNALQUALITY_SUCCEEDED:
		case ME_SIGNAL_QUALITY_CHANGED:
			modem_update_signal_quality(modem);
			return;
		case ME_GETINFO_SUCCEEDED:
		case ME_GETINFO_FAILED:
		case ME_GET_PROPERTIES_SUCCEEDED:
		case ME_GET_PROPERTIES_FAILED:
			modem_task_run(modem);
			return;
		default:
			break;
		}
		_DBG_MODEMMGR("Invalid event %s for state ENABLED", event_name);
		return;
	case MODEM_STATE_REGISTERED:
		switch (event) {
		case ME_REGISTRATION_STATE_CHANGED:
			modem_registration_state_change(modem);
			return;
		case ME_DISABLE_SUCCEEDED:
			modem_set_powered(modem, FALSE);
			modem->state = MODEM_STATE_DISABLED;
			modem_clear_registration_state(modem);
			modem_registration_state_change(modem);
			modem_task_run(modem);
			return;
		case ME_DISABLE_FAILED:
			modem_set_powered(modem, TRUE);
			modem_task_completed(&modem);
			return;
		case ME_GET_REGISTRATION_STATE_SUCCEEDED:
			/*
			 * Complete the enable task first.  Then
			 * process the registration change, which will
			 * create a connman network object and may
			 * auto connect.
			 */
			modem_task_run(modem);  /* process modem_report_enabled_task */
			if (modem != NULL)
				modem_registration_state_change(modem);
			return;
		case ME_GET_REGISTRATION_STATE_FAILED:
			modem_task_completed(&modem);
			return;
		case ME_GET_SIGNALQUALITY_FAILED:
			/* N.B. We can continue even without signal quality */
			return;
		case ME_GET_SIGNALQUALITY_SUCCEEDED:
		case ME_SIGNAL_QUALITY_CHANGED:
			modem_update_signal_quality(modem);
			return;
		case ME_GETINFO_SUCCEEDED:
		case ME_GETINFO_FAILED:
		case ME_GET_PROPERTIES_SUCCEEDED:
		case ME_GET_PROPERTIES_FAILED:
			modem_task_run(modem);
			return;
		default:
			break;
		}
		_DBG_MODEMMGR("Invalid event %s for state REGISTERED", event_name);
		return;
	case MODEM_STATE_CONNECTED:
		switch (event) {
		case ME_REGISTRATION_STATE_CHANGED:
			modem_registration_state_change(modem);
			return;
		case ME_DISCONNECT_SUCCEEDED:
			/*
			 * May or may not transition through the
			 * DISCONNECTING state before getting to
			 * DISCONNECTED
			 */
			modem_set_connected(modem, TRUE);
			modem->state = MODEM_STATE_REGISTERED;
			modem_set_connected(modem, FALSE);
			modem_task_run(modem);
			return;
		case ME_DISCONNECT_FAILED:
			modem_task_completed(&modem);
			return;
		case ME_DISABLE_SUCCEEDED:
			handle_reported_disconnect(modem);
			modem_set_powered(modem, FALSE);
			modem->state = MODEM_STATE_DISABLED;
			modem_clear_registration_state(modem);
			modem_registration_state_change(modem);
			modem_task_run(modem);
			return;
		case ME_DISABLE_FAILED:
			modem_set_powered(modem, TRUE);
			modem_task_completed(&modem);
			return;
		case ME_GETIP4CONFIG_SUCCEEDED:
		case ME_GETIP4CONFIG_FAILED:
			/* TODO(jglasgow): implement.... */
			modem_task_run(modem);
			break;
		case ME_GET_SIGNALQUALITY_FAILED:
			/* N.B. We can continue even without signal quality */
			return;
		case ME_GET_SIGNALQUALITY_SUCCEEDED:
		case ME_SIGNAL_QUALITY_CHANGED:
			modem_update_signal_quality(modem);
			return;
		default:
			break;
		}
		_DBG_MODEMMGR("Invalid event %s for state CONNECTED\n", event_name);
		return;
	case MODEM_STATE_FAILED:
		_DBG_MODEMMGR("Invalid event %s for state FAILED", event_name);
		return;
	case MODEM_STATE_REMOVED:
		_DBG_MODEMMGR("Ignoring event %s for state REMOVED", event_name);
		return;
	default:
		_DBG_MODEMMGR("Invalid state %s", state_name);
	}
}

static const char *eventToString(enum modem_event event)
{
	static char text[32];
	switch (event) {
	case ME_NO_EVENT: return "NO_EVENT";
	case ME_PROPERTIES_CHANGED: return "PROPERTIES_CHANGED";
	case ME_SIGNAL_QUALITY_CHANGED: return "SIGNAL_QUALITY_CHANGED";
	case ME_REGISTRATION_STATE_CHANGED: return "REGISTRATION_STATE_CHANGED";
	case ME_ENABLE_SUCCEEDED: return "ENABLE_SUCCEEDED";
	case ME_ENABLE_FAILED: return "ENABLE_FAILED";
	case ME_REGISTER_SUCCEEDED: return "REGISTER_SUCCEEDED";
	case ME_REGISTER_FAILED: return "REGISTER_FAILED";
	case ME_DISABLE_SUCCEEDED: return "DISABLE_SUCCEEDED";
	case ME_DISABLE_FAILED: return "DISABLE_FAILED";
	case ME_GETIP4CONFIG_SUCCEEDED: return "GETIP4CONFIG_SUCCEEDED";
	case ME_GETIP4CONFIG_FAILED: return "GETIP4CONFIG_FAILED";
	case ME_GETINFO_SUCCEEDED: return "GETINFO_SUCCEEDED";
	case ME_GETINFO_FAILED: return "GETINFO_FAILED";
	case ME_GET_PROPERTIES_SUCCEEDED: return "GET_PROPERTIES_SUCCEEDED";
	case ME_GET_PROPERTIES_FAILED: return "GET_PROPERTIES_FAILED";
	case ME_DISCONNECT_SUCCEEDED: return "DISCONNECT_SUCCEEDED";
	case ME_DISCONNECT_FAILED: return "DISCONNECT_FAILED";
	case ME_GET_SIGNALQUALITY_SUCCEEDED: return "GET_SIGNALQUALITY_SUCCEEDED";
	case ME_GET_SIGNALQUALITY_FAILED: return "GET_SIGNALQUALITY_FAILED";
	case ME_GET_SERVING_SYSTEM_SUCCEEDED: return "GET_SERVING_SYSTEM_SUCCEEDED";
	case ME_GET_SERVING_SYSTEM_FAILED: return "GET_SERVING_SYSTEM_FAILED";
	case ME_GET_REGISTRATION_STATE_SUCCEEDED: return "GET_REGISTRATION_STATE_SUCCEEDED";
	case ME_GET_REGISTRATION_STATE_FAILED: return "GET_REGISTRATION_STATE_FAILED";
	case ME_CREATE_DEVICE_FAILED: return "CREATE_DEVICE_FAILED";
	default:
		g_snprintf(text, sizeof(text), "Unknown event %d", event);
		return text;
	}
}

static const char *stateToString(enum modem_state state)
{
	static char text[32];
	switch (state) {
	case MODEM_STATE_UNINITIALIZED: return "UNINITIALIZED";
	case MODEM_STATE_DISABLED: return "DISABLED";
	case MODEM_STATE_ENABLED: return "ENABLED";
	case MODEM_STATE_REGISTERED: return "REGISTERED";
	case MODEM_STATE_CONNECTED: return "CONNECTED";
	case MODEM_STATE_FAILED: return "FAILED";
	case MODEM_STATE_REMOVED: return "REMOVED";
	default:
		g_snprintf(text, sizeof(text), "Unknown state %d", state);
		return text;
	}
}

static enum connman_network_activation_state convert_activation_state(
		guint mm_activation_state)
{
	switch (mm_activation_state) {
	case MM_MODEM_CDMA_ACTIVATION_STATE_NOT_ACTIVATED:
		return CONNMAN_NETWORK_ACTIVATION_STATE_NOT_ACTIVATED;
	case MM_MODEM_CDMA_ACTIVATION_STATE_ACTIVATING:
		return CONNMAN_NETWORK_ACTIVATION_STATE_ACTIVATING;
	case MM_MODEM_CDMA_ACTIVATION_STATE_PARTIALLY_ACTIVATED:
		return CONNMAN_NETWORK_ACTIVATION_STATE_PARTIALLY_ACTIVATED;
	case MM_MODEM_CDMA_ACTIVATION_STATE_ACTIVATED:
		return CONNMAN_NETWORK_ACTIVATION_STATE_ACTIVATED;
	default:
		return CONNMAN_NETWORK_ACTIVATION_STATE_NOT_ACTIVATED;
	}
}

static enum connman_element_error convert_activation_error(
		guint mm_activation_error)
{
	switch (mm_activation_error) {
	case MM_MODEM_CDMA_ACTIVATION_ERROR_WRONG_RADIO_INTERFACE:
		return CONNMAN_ELEMENT_ERROR_NEED_EVDO;
	case MM_MODEM_CDMA_ACTIVATION_ERROR_ROAMING:
		return CONNMAN_ELEMENT_ERROR_NEED_HOME_NETWORK;
	case MM_MODEM_CDMA_ACTIVATION_ERROR_COULD_NOT_CONNECT:
	case MM_MODEM_CDMA_ACTIVATION_ERROR_SECURITY_AUTHENTICATION_FAILED:
	case MM_MODEM_CDMA_ACTIVATION_ERROR_PROVISIONING_FAILED:
		return CONNMAN_ELEMENT_ERROR_OTASP_FAILED;
	case MM_MODEM_CDMA_ACTIVATION_ERROR_NO_ERROR:
		return CONNMAN_ELEMENT_ERROR_NO_ERROR;
	case MM_MODEM_CDMA_ACTIVATION_ERROR_NO_SIGNAL:
	default:
		return CONNMAN_ELEMENT_ERROR_ACTIVATION_FAILED;
	}
}

/**
 * add_modem
 * @path: dbus path to the modem
 *
 * Add a modem to the list of modems, and then send a dbus message to
 * find the modem properties.  Once the properties are known, we will
 * create a connman_device to associate with the modem data.
 *
 */
static void add_modem(const char *owner, const char *service, const char *path)
{
	struct modem_data *modem = NULL;

	_DBG_MODEMMGR("adding device path %s owner %s", path, owner);

	modem = g_hash_table_lookup(modem_hash, path);
	if (modem != NULL) {
		_DBG_MODEMMGR("found existing modem with path %s", path);
		return;
	}

	modem = g_try_new0(struct modem_data, 1);
	if (modem == NULL) {
		_DBG_MODEMMGR("failed to allocate modem for path %s", path);
		return;
	}
	modem->owner = g_strdup(owner);
	if (modem->owner == NULL) {
		_DBG_MODEMMGR("failed to allocate owner for modem %s", path);
		g_free(modem);
		return;
	}
	modem->service = g_strdup(service);
	if (modem->service == NULL) {
		_DBG_MODEMMGR("failed to allocate service for modem %s", path);
		g_free(modem->owner);
		g_free(modem);
		return;
	}
	modem->dbus_path = g_strdup(path);
	if (modem->dbus_path == NULL) {
		_DBG_MODEMMGR("failed to allocate path for modem %s", path);
		g_free(modem->service);
		g_free(modem->owner);
		g_free(modem);
		return;
	}

	/* TODO(jglasgow): read profile to get phone_number, handle ENOMEM */
	modem->type = 0;
	modem->state = MODEM_STATE_UNINITIALIZED;
	modem->signal_strength = 0;
	modem->network_technology = CONNMAN_NETWORK_TECHNOLOGY_UNKNOWN;
	/* Set IP method to an invalid type */
	modem->ip_method = -1;

	/* Update the list of devices */
	g_hash_table_insert(modem_hash, modem->dbus_path,  modem);

	/* collect modem properties */
	modem_task_run_new(modem, &initialize_modem_work);

	_DBG_MODEMMGR("added device modem %p", modem);
}

/**
 * Callback from modem_hash on device removal
 */
static void remove_modem(gpointer data)
{
	struct modem_data *modem = data;

	_DBG_MODEMMGR("removing device modem %p", modem);

	/* cleanup and delete a modem device object */
	if (modem->disconnect_needed) {
		modem->disconnect_needed = FALSE;
		handle_reported_disconnect(modem);
	}
	if (modem->cm_device != NULL) {
		connman_device_set_powered(modem->cm_device, FALSE);
		connman_device_unregister(modem->cm_device);
		connman_device_unref(modem->cm_device);
	}
	modem_dbus_cancel_pending_calls(modem);
	g_free(modem->owner);
	g_free(modem->service);
	g_free(modem->dbus_path);
	g_free(modem->phone_number);
	g_free(modem->olp.url);
	g_free(modem->olp.method);
	g_free(modem->olp.postdata);
	g_free(modem->usage_url);
	g_free(modem->device);
	g_free(modem->driver);
	g_free(modem->master_device);
	g_free(modem->network_dbus_name);
	g_free(modem->configured_carrier);
	g_free(modem->gsm.operator_name);
	g_free(modem->gsm.operator_country);
	g_free(modem->gsm.network_id);
	g_free(modem->gsm.next_apn.apn);
	g_free(modem->gsm.next_apn.network_id);
	g_free(modem->gsm.next_apn.username);
	g_free(modem->gsm.next_apn.password);
	g_free(modem->meid);
	g_free(modem->imei);
	g_free(modem->imsi);
	g_free(modem);
}

/**
 * remove_device:
 * @path: dbus path to the device
 *
 * Remove a device from the list of modem devices
 */
static void remove_device(const char *path)
{
	struct modem_data *modem;

	_DBG_MODEMMGR("removing device path %s", path);

	modem = g_hash_table_lookup(modem_hash, path);

	if (modem == NULL) {
		_DBG_MODEMMGR("cannot remove modem with path %s", path);
		return;
	}
	if (modem->pending_task) {
		/* A pending task means there are outstanding DBUS
		 * messages that have a pointer to the modem object.
		 * Mark the object to be removed later.
		 */
		modem->task.remove_on_complete = TRUE;
	} else {
		if (modem->state == MODEM_STATE_CONNECTED)
			modem->disconnect_needed = TRUE;
		modem->state = MODEM_STATE_REMOVED;

		/* remove from list of devices causing its destruction */
		g_hash_table_remove(modem_hash, path);
	}
}

/* ===================== */
/* Network object for 3G */
/* ===================== */

static struct modem_data *network_get_modem(struct connman_network *network,
					    const char *func)
{
	struct connman_device *device;

	device = connman_network_get_device(network);
	if (device == NULL) {
		return NULL;
	}
	return modem_device_find_modem(device, func);
}


static int network_probe(struct connman_network *network)
{
	const char *path;

	path = connman_network_get_string(network, "DBus.Object");

	_DBG_MODEMMGR("network %p path %s", network, path);

	return 0;
}

static int network_connect(struct connman_network *network)
{
	int err;
	struct modem_data *modem;
	enum connman_network_cellular_roaming_state roaming_state;

	_DBG_MODEMMGR("network %p", network);

	modem = network_get_modem(network, __func__);
	if (modem == NULL) {
		connman_network_set_disconnected(network);
		return -ENODEV;
	}
	if (modem->state == MODEM_STATE_CONNECTED) {
		return -EISCONN;
	}
	roaming_state = connman_network_get_roaming_state(network);
	if (!connman_device_roaming_allowed(modem->cm_device) &&
	    roaming_state == CONNMAN_NETWORK_ROAMING_STATE_ROAMING) {
		connman_error("%s: failing connect: "
			      "roaming connections not allowed",
			      __func__);
		return -ENONET;
	}

	reset_connect_state(modem);
	err = modem_connect_start(modem);
	if (err != -EINPROGRESS) {
		connman_network_set_disconnected(network);
		return -EIO;
	}
	return -EINPROGRESS;
}

static int network_disconnect(struct connman_network *network)
{
	struct modem_data *modem;

	_DBG_MODEMMGR("network %p index %d", network,
		      connman_network_get_index(network));

	modem = network_get_modem(network, __func__);
	if (modem == NULL) {
		return -ENODEV;
	}
	if (modem->state != MODEM_STATE_CONNECTED) {
		return -EALREADY;
	}
	return modem_task_run_new(modem, &disconnect_modem_work);
}

static void network_remove(struct connman_network *network)
{
	_DBG_MODEMMGR("network %p", network);
}

/**
 * Activate the modem on the network.
 *
 */
static int modem_network_activate(struct connman_network *network,
	const char *carrier)
{
	struct modem_data *modem;
	DBusMessage *message;

	modem = network_get_modem(network, __func__);
	if (modem == NULL) {
		return -ENODEV;
	}
	if (modem->type != MM_MODEM_TYPE_CDMA) {
		_DBG_MODEMMGR("Cannot activate non-CDMA modem\n");
		return -EINVAL;
	}
	switch (modem->state) {
	case MODEM_STATE_ENABLED:
	case MODEM_STATE_REGISTERED:
		break;
	default:
		_DBG_MODEMMGR("Cannot activate in state: %s\n",
			      stateToString(modem->state));
		return -EIO;
	}

	if (modem_dbus_build_message(modem->owner, MM_MODEM_CDMA_INTERFACE,
				     modem->dbus_path, &message,
				     MM_MODEM_CDMA_METHOD_ACTIVATE) < 0) {
		_DBG_MODEMMGR("activate modem %s", modem->dbus_path);
		return -EIO;
	}

	_DBG_MODEMMGR("activate modem %s", modem->dbus_path);
	dbus_message_append_args(message,
				 DBUS_TYPE_STRING, &carrier,
				 DBUS_TYPE_INVALID);

	if (modem_dbus_send_with_reply(modem,
				       message,
				       modem,
				       modem_activate_reply,
				       ACTIVATE_TIMEOUT_MS) < 0) {
		_DBG_MODEMMGR("activate modem %s", modem->dbus_path);
		return -EIO;
	}
	modem_update_activation_state(modem,
		CONNMAN_NETWORK_ACTIVATION_STATE_ACTIVATING,
		CONNMAN_ELEMENT_ERROR_NO_ERROR);

	return 0;
}

static struct connman_network_driver network_driver = {
	.name		= "network",
	.type		= CONNMAN_NETWORK_TYPE_CELLULAR,
	.probe		= network_probe,
	.remove		= network_remove,
	.connect	= network_connect,
	.disconnect	= network_disconnect,
	/* .setup		= network_setup,  upstream */
	.activate_cellular_network = modem_network_activate,
};



/*==========================================================
 * Handlers for modem manager root object calls and signals
 *==========================================================*/

static void enumerate_devices_reply(DBusPendingCall *call,
				    void *user_data)
{
	struct modem_manager *manager = (struct modem_manager *)user_data;
	DBusMessage *reply;
	DBusError error;
	char **results;
	int i, num_results;
	const char *sender;

	_DBG_MODEMMGR("enumerate_devices_reply: %p", call);

	reply = dbus_pending_call_steal_reply(call);
	if (reply == NULL) {
		CONNMAN_ERROR("Failed steal reply");
		return;
	}
	dbus_error_init(&error);
	sender = dbus_message_get_sender(reply);

	if (dbus_set_error_from_message(&error, reply)) {
		CONNMAN_ERROR("DBus Error: %s:%s",
			      error.name, error.message);
		goto done;
	}

	/*
	 * Since this is the first message we receive from a modem manager,
	 * record the unique bus name associated with the manager, so that
	 * we can later tell which modem manager owns a given modem.
	 */
	if (manager->bus_name == NULL)
		manager->bus_name = g_strdup(sender);

	if (dbus_message_get_args(reply, &error,
				  DBUS_TYPE_ARRAY, DBUS_TYPE_OBJECT_PATH,
				  &results, &num_results,
				  DBUS_TYPE_INVALID) == FALSE) {
		if (dbus_error_is_set(&error) == TRUE) {
			CONNMAN_ERROR("Unpacking error: %s:%s",
				      error.name, error.message);
		} else {
			CONNMAN_ERROR("Wrong arguments for enumerate_devices");
		}
		goto done;
	}

	if (num_results == 0) {
		_DBG_MODEMMGR("no modems returned");
		goto done;
	}

	for (i = 0; i < num_results; i++) {
		/* Add device will remove duplicates */
		add_modem(sender, manager->service, results[i]);
	}
	/* TODO(jglasgow): consider removing devices that no longer exist */

	g_strfreev(results);

done:
	dbus_error_free(&error);
	dbus_message_unref(reply);
}


static void cdma_registration_state_changed(
	struct modem_data *modem,
	DBusMessage *msg)
{
	DBusMessageIter iter;

	dbus_message_iter_init(msg, &iter);
	dbus_message_iter_get_basic(&iter, &modem->cdma._1x_registration_state);
	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &modem->cdma.evdo_registration_state);
	dbus_message_iter_next(&iter);
	modem_handle_event(modem, ME_REGISTRATION_STATE_CHANGED);
}


static void signal_quality_changed(
	struct modem_data *modem,
	DBusMessage *msg)
{
	DBusMessageIter iter;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &modem->signal_strength);
	dbus_message_iter_next(&iter);
	modem_handle_event(modem, ME_SIGNAL_QUALITY_CHANGED);
}

static void network_mode_changed(
	struct modem_data *modem,
	DBusMessage *msg)
{
	DBusMessageIter iter;
	int network_mode;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &network_mode);
	dbus_message_iter_next(&iter);

	_DBG_MODEMMGR("%s: network_mode = 0x%x",
		      modem->dbus_path, network_mode);
	/* TODO(jglasgow): add as a property of the network */
}

static void gsm_registration_state_changed(
	struct modem_data *modem,
	DBusMessage *msg)
{
	DBusMessageIter iter;
	const char *network_id, *operator_name;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &modem->gsm.registration_state);
	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &network_id);
	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &operator_name);
	g_free(modem->gsm.operator_name);
	modem->gsm.operator_name = g_strdup(operator_name);
	set_network_id(modem, network_id);
	dbus_message_iter_next(&iter);

	modem_handle_event(modem, ME_REGISTRATION_STATE_CHANGED);
}

/**
 * Update flimflam state after the modem manager has told us that
 * the modem is in the connected state. If we're roaming, and
 * data roaming is not being allowed, forcefully disconnect,
 * after getting our state in sync with the modem manager's.
 */
static void handle_reported_connect(struct modem_data *modem)
{
	struct connman_network *network = get_network(modem);

	modem->connect_needed = FALSE;
	if (network != NULL && connman_network_get_connected(network) == FALSE) {
		modem_set_connected(modem, TRUE);
		if (modem->gsm.registration_state ==
		        MM_MODEM_GSM_NETWORK_REG_STATUS_ROAMING &&
		    !connman_device_roaming_allowed(modem->cm_device)) {
			connman_error("%s: disconnecting: "
				      "roaming connections not allowed",
				      __func__);
			modem_task_run_new(modem, &disconnect_modem_work);
		}
	}
}

/**
 * Update flimflam state after the modem manager has told us that
 * the modem is in the disconnected state.
 */
static void handle_reported_disconnect(struct modem_data *modem)
{
	struct connman_network *network = get_network(modem);

	if (network != NULL) {
		connman_network_set_disconnecting(network);
		modem_set_connected(modem, FALSE);
	}
}

static void modem_state_changed(struct modem_data *modem, DBusMessage *msg) {
	DBusMessageIter iter;
	dbus_uint32_t old_state;
	dbus_uint32_t new_state;
	dbus_uint32_t reason = MM_MODEM_STATE_CHANGED_REASON_UNKNOWN;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &old_state);
	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &new_state);
	dbus_message_iter_next(&iter);

	if (old_state == new_state) {
		/* Warn on state transitions that should not be sent */
		CONNMAN_ERROR("Invalid state_change %d -> %d",
			      old_state, new_state);
		return;
	}

	if (new_state == MM_MODEM_STATE_REGISTERED ||
	    new_state == MM_MODEM_STATE_ENABLED)
		dbus_message_iter_get_basic(&iter, &reason);

	_DBG_MODEMMGR("%u --> %u: %u", old_state, new_state, reason);
	switch (new_state) {
	case MM_MODEM_STATE_DISABLED:
		/*
		 * This state change may be the modem manager
		 * updating us on the status of an Disable operation
		 * that we initiated, in which case the call to
		 * modem_device_disable() will be a no-op, and will
		 * result in a message being printed when debug
		 * logging is turned on.
		 */
		modem_handle_event(modem, ME_DISABLE_SUCCEEDED);
		break;
	case MM_MODEM_STATE_ENABLED:
		/*
		 * This state change may be the modem manager
		 * updating us on the status of an Enable operation
		 * that we initiated, in which case the call to
		 * modem_device_enable() will be a no-op and will
		 * result in a message being printed when debug
		 * logging is turned on.
		 */
		if (old_state == MM_MODEM_STATE_DISABLED ||
		    old_state == MM_MODEM_STATE_ENABLING) {
			modem_device_enable(modem->cm_device);
			break;
		}
		/* ... fall into */
	case MM_MODEM_STATE_REGISTERED:
		/*
		 * Check the old state to avoid treating
		 * enable->register transitions as a disconnect
		 */
		if (old_state == MM_MODEM_STATE_CONNECTED ||
		    old_state == MM_MODEM_STATE_DISCONNECTING) {
			handle_reported_disconnect(modem);
			if (modem->pending_task)
				modem_task_run(modem);
		} else if (old_state == MM_MODEM_STATE_REGISTERED) {
			/* Because of check above, new_state must be ENABLED */
			modem->gsm.registration_state =
					MM_MODEM_GSM_NETWORK_REG_STATUS_IDLE;
			modem_registration_state_change(modem);
		}
		break;
	case MM_MODEM_STATE_SEARCHING:
		if (old_state == MM_MODEM_STATE_REGISTERED) {
			modem->gsm.registration_state =
					MM_MODEM_GSM_NETWORK_REG_STATUS_SEARCHING;
			modem_registration_state_change(modem);
		} else if (old_state == MM_MODEM_STATE_ENABLING) {
			/* Sometimes modem-manager will go straight from
			   ENABLING to SEARCHING; handle that as we handle
			   the transition from ENABLING to ENABLED.  (We
			   don't do anything special for the ENABLED to
			   SEARCHING transition.) */
			modem_device_enable(modem->cm_device);
		}
		break;
	case MM_MODEM_STATE_CONNECTED:
		/* No need to check old state */
		handle_reported_connect(modem);
		if (modem->pending_task)
			modem_task_run(modem);
		break;
	default:
		CONNMAN_ERROR("Unhandled state_change %d -> %d",
			      old_state, new_state);
	}
}

static void modem_activation_state_changed(struct modem_data *modem,
					   DBusMessage *msg) {
	DBusMessageIter iter;
	dbus_uint32_t mm_state;
	dbus_uint32_t mm_error;
	char *min = NULL;
	char *mdn = NULL;
	char *olp = NULL;
	gint extract_err = 0;
	gint type;
	DBusMessageIter dict;
	connman_bool_t olp_changed = FALSE;

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &mm_state);
	dbus_message_iter_next(&iter);
	dbus_message_iter_get_basic(&iter, &mm_error);
	dbus_message_iter_next(&iter);
	_DBG_MODEMMGR("state = %u error = %u", mm_state, mm_error);
	if ((type = dbus_message_iter_get_arg_type(&iter)) != DBUS_TYPE_ARRAY) {
		_DBG_MODEMMGR("Expected array, got %x", type);
		goto update;
	}
	dbus_message_iter_recurse(&iter, &dict);

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY &&
	       extract_err == 0) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);
		_DBG_MODEMMGR("Saw key: %s", key);

		if (modem_dbus_extract_string("mdn", key, &value, &mdn,
					      &extract_err)) {
			connman_device_set_string(modem->cm_device,
						  "Cellular.MDN", mdn);
		} else if (modem_dbus_extract_string("min", key, &value, &min,
				&extract_err)) {
			connman_device_set_string(modem->cm_device,
						  "Cellular.MIN", min);
		} else if (modem_dbus_extract_string(
				"payment_url", key, &value, &olp,
				&extract_err)) {
			g_free(modem->olp.url);
			modem->olp.url = g_strdup(olp);
			olp_changed = TRUE;
		} else if (modem_dbus_extract_string(
				"payment_url_method", key, &value, &olp,
				&extract_err)) {
			g_free(modem->olp.method);
			modem->olp.method = g_strdup(olp);
			olp_changed = TRUE;
		} else if (modem_dbus_extract_string(
				"payment_url_postdata", key, &value, &olp,
				&extract_err)) {
			g_free(modem->olp.postdata);
			modem->olp.postdata = g_strdup(olp);
			olp_changed = TRUE;
		}
		dbus_message_iter_next(&dict);
	}
	if (olp_changed) {
		struct connman_network *network = get_network(modem);
		if (network != NULL)
			connman_network_set_olp_url(network,
						    modem->olp.url,
						    modem->olp.method,
						    modem->olp.postdata);
	}

      update:
	modem_update_activation_state(modem,
				      convert_activation_state(mm_state),
				      convert_activation_error(mm_error));
}

static void mm_properties_changed(struct modem_data *modem,
				  DBusMessage *msg)
{
	DBusMessageIter iter;
	DBusMessageIter dict;
	char *interface;
	guint technology;
	dbus_uint32_t facility_locks = 0;
	connman_bool_t saw_unlock_props = FALSE;
	struct connman_network *network = get_network(modem);

	dbus_message_iter_init(msg, &iter);

	dbus_message_iter_get_basic(&iter, &interface);
	dbus_message_iter_next(&iter);
	_DBG_MODEMMGR("MmPropertiesChanged for interface %s", interface);

	gint type;
	if ((type = dbus_message_iter_get_arg_type(&iter)) != DBUS_TYPE_ARRAY) {
		_DBG_MODEMMGR("Expected array, got %x", type);
		return;
	}

	dbus_message_iter_recurse(&iter, &dict);
	gint extract_err = 0;

	while (dbus_message_iter_get_arg_type(&dict) == DBUS_TYPE_DICT_ENTRY &&
	       extract_err == 0) {
		DBusMessageIter entry, value;
		const char *key;

		dbus_message_iter_recurse(&dict, &entry);
		dbus_message_iter_get_basic(&entry, &key);

		dbus_message_iter_next(&entry);
		dbus_message_iter_recurse(&entry, &value);
		_DBG_MODEMMGR("  Saw property key: %s", key);

		if (modem_dbus_extract_uint32("UnlockRetries", key,
					     &value, &modem->gsm.lock.retries,
					     &extract_err))
			saw_unlock_props = TRUE;
		else if (modem_dbus_extract_string("UnlockRequired", key,
				     &value, &modem->gsm.lock.unlock_required,
				     &extract_err))
			saw_unlock_props = TRUE;
		else if (modem_dbus_extract_uint32("EnabledFacilityLocks", key,
				   &value, &facility_locks, &extract_err)) {
			modem->gsm.lock.enabled =
				(facility_locks & MM_MODEM_GSM_FACILITY_SIM) != 0;
			saw_unlock_props = TRUE;
		} else if (network != NULL &&
			 modem_dbus_extract_uint32("AccessTechnology", key,
					   &value, &technology, &extract_err)) {
			_DBG_MODEMMGR("  value: %u", technology);
			enum connman_network_cellular_technology tech =
				mm_gsm_technology_to_connman_technology(technology);
			connman_network_set_registration_info(
					network, tech,
					CONNMAN_NETWORK_ROAMING_STATE_UNKNOWN);
		}
		dbus_message_iter_next(&dict);
	}
	if (saw_unlock_props && modem->cm_device != NULL)
		connman_device_set_unlock_properties(modem->cm_device,
						     modem->gsm.lock.unlock_required,
						     modem->gsm.lock.retries,
						     modem->gsm.lock.enabled);
}

static DBusHandlerResult modemmgr_filter(DBusConnection *conn,
					 DBusMessage *msg,
					 void *data)
{
	const char *member, *path, *interface;
	struct modem_data *modem;

	/*
	 * Bail out quickly if the message is not one in which
	 * we are interested.
	 */
	interface = dbus_message_get_interface(msg);
	if (interface == NULL)
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	/* The signal that has been sent */
	member = dbus_message_get_member(msg);
	if (member == NULL) {
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	if (!g_str_has_prefix(interface, MM_MODEMMANAGER_INTERFACE) &&
	    !g_str_equal(member, MM_MANAGER_SIGNAL_MMPROPERTIESCHANGED)) {
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	/* The path of the device (sending the message) */
	path = dbus_message_get_path(msg);
	if (path == NULL) {
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	_DBG_MODEMMGR("path %s member %s", path, member);

	if (g_strcmp0(interface, MM_MODEMMANAGER_INTERFACE) == 0) {
		/* TODO(jglasgow): make sure type is correct */
		DBusMessageIter iter;
		const char *modem_path;
		const char *sender = dbus_message_get_sender(msg);

		if (sender == NULL) {
			_DBG_MODEMMGR("unknown sender");
			return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
		}

		dbus_message_iter_init(msg, &iter);
		dbus_message_iter_get_basic(&iter, &modem_path);
		dbus_message_iter_next(&iter);

		_DBG_MODEMMGR("member %s path %s modem path %s sender %s",
			      member, path, modem_path, sender);

		if (g_str_equal(member, MM_MANAGER_SIGNAL_DEVICEADDED)) {
			/* Convert the manager root object name to a service name. */
			char *service = g_strdup(path+1);

			/* Remove trailing '/'. */
			int len = strlen(service);
			if (len > 0 && service[len-1] == '.')
				service[len-1] = '\0';

			/* Map '/' to '.'. */
			char *p = strchr(service, '/');
			while (p) {
				*p = '.';
				p = strchr(p+1, '/');
			}
			add_modem(sender, service, modem_path);
			g_free(service);
		} else if (g_str_equal(member,
					MM_MANAGER_SIGNAL_DEVICEREMOVED)) {
			remove_device(modem_path);
		} else {
			_DBG_MODEMMGR("unknown signal %s", member);
		}
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	/* Signals on the other interfaces must correspond to a modem object */
	modem = g_hash_table_lookup(modem_hash, path);
	if (modem == NULL) {
		return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
	}

	if (g_strcmp0(interface, MM_MODEM_INTERFACE) == 0) {
		if (g_str_equal(member,
		                MM_MODEM_SIGNAL_STATECHANGED)) {
			modem_state_changed(modem, msg);
		}
	} else if (g_strcmp0(interface, MM_MODEM_CDMA_INTERFACE) == 0) {
		if (g_str_equal(member,
				MM_MODEM_CDMA_SIGNAL_REGISTRATIONSTATECHANGED))
			cdma_registration_state_changed(modem, msg);
		else if (g_str_equal(member,
				     MM_MODEM_CDMA_SIGNAL_SIGNALQUALITY))
			signal_quality_changed(modem, msg);
		else if (g_str_equal(member,
		                     MM_MODEM_CDMA_SIGNAL_ACTIVATIONSTATECHANGED))
			modem_activation_state_changed(modem, msg);
	} else if (g_strcmp0(interface,
			     MM_MODEM_GSM_NETWORK_INTERFACE) == 0) {
		if (g_str_equal(member,
				MM_MODEM_GSM_NETWORK_SIGNAL_REGISTRATIONINFO))
			gsm_registration_state_changed(modem, msg);
		else if (g_str_equal(member,
				     MM_MODEM_GSM_NETWORK_SIGNAL_SIGNALQUALITY))
			signal_quality_changed(modem, msg);
		else if (g_str_equal(member,
				     MM_MODEM_GSM_NETWORK_SIGNAL_NETWORKMODE))
			network_mode_changed(modem, msg);
	} else if (g_strcmp0(interface,
			     MM_DBUS_PROPERTIES_INTERFACE) == 0) {
		if (g_str_equal(member, MM_MANAGER_SIGNAL_MMPROPERTIESCHANGED)) {
			mm_properties_changed(modem, msg);
		}
	}

	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
}

static const char *modemmgr_rule =
	"type=signal,interface=" MM_MODEMMANAGER_INTERFACE;
static const char *modem_rule = "type=signal,interface=" MM_MODEM_INTERFACE;
static const char *cdma_rule = "type=signal,interface=" MM_MODEM_CDMA_INTERFACE;
static const char *gsm_network_rule =
	"type=signal,interface=" MM_MODEM_GSM_NETWORK_INTERFACE;
static const char *mm_properties_rule =
	"type=signal,interface=" MM_DBUS_PROPERTIES_INTERFACE
	",member=MmPropertiesChanged";

/**
 * modemmgr_connect: Handle a new connection from modem manager
 *
 * Set up filters to get signals from modem manager. Enumerate the
 * modems.
 */

static void modemmgr_connect(DBusConnection *connection, void *user_data)
{
	struct modem_manager *manager = (struct modem_manager *)user_data;
	DBusMessage *message;
	DBusPendingCall *call;

	_DBG_MODEMMGR("service %s", manager->service);

	/* Enumerate the devices (modems) */
	message = dbus_message_new_method_call(
			manager->service,
			manager->path,
			MM_MODEMMANAGER_INTERFACE,
			MM_MANAGER_METHOD_ENUMERATEDEVICES);
	if (message == NULL)
		return;

	if (dbus_connection_send_with_reply(connection, message,
				    &call, DEFAULT_TIMEOUT_MS) == FALSE) {
		CONNMAN_ERROR("Failed to get modem devices");
		goto done;
	}

	if (call == NULL) {
		CONNMAN_ERROR("D-Bus connection not available");
		goto done;
	}

	dbus_pending_call_set_notify(call, enumerate_devices_reply,
				     manager, NULL);
	dbus_pending_call_unref(call);

done:
	dbus_message_unref(message);
}

static void maybe_remove_modem(gpointer key,
			       gpointer value,
			       gpointer user_data)
{
	struct modem_data *modem = value;
	char *bus_name = user_data;

	_DBG_MODEMMGR("modem %s busname %s owner %s", (char *)key, bus_name,
	              modem->owner);
	if (bus_name != NULL && g_strcmp0(modem->owner, bus_name) == 0) {
		if (modem->pending_task)
			modem->task.remove_on_complete = TRUE;
		else
			remove_device((const char *)key);
	}
}

static void modemmgr_disconnect(DBusConnection *connection, void *user_data)
{
	struct modem_manager *manager = (struct modem_manager *)user_data;
	_DBG_MODEMMGR("service %s", manager->service);
	/*
	 * A modem manager disappeared. Remove all modems that the
	 * manager was tracking.
	 */
	g_hash_table_foreach(modem_hash, maybe_remove_modem, manager->bus_name);
	g_free(manager->bus_name);
	manager->bus_name = NULL;
}

/**
 * modem_manager_new
 *
 * Create a new modem_manager object which stores the service name and
 * path used to communicate with a modem manager dbus server.
 *
 * Returns:
 *   pointer to initialized modem_manager object or NULL
 */
static struct modem_manager *modem_manager_new(const char *service,
					       const char *path)
{
	struct modem_manager *manager = g_try_new0(struct modem_manager, 1);

	if (manager == NULL) {
		return NULL;
	}
	manager->service = g_strdup(service);
	if (manager->service == NULL) {
		g_free(manager);
		return NULL;
	}
	manager->path = g_strdup(path);
	if (manager->path == NULL) {
		g_free(manager->service);
		g_free(manager);
		return NULL;
	}
	return manager;
}

/**
 * modem_manager_free
 *
 * release all resources associated with a modem_manager object
 */
static void modem_manager_free(void *user_data)
{
	struct modem_manager *manager = (struct modem_manager *)user_data;

	g_free(manager->service);
	g_free(manager->path);
	g_free(manager->bus_name);
	g_free(manager);
}

/**
 * modemmgr_watch_mm_services
 *
 * set up dbus callback functions to handle the appearance and
 * disappearance of modem manager
 *
 * Args:
 *   managers: comma separated list of service:path values
 */
static int modemmgr_watch_mm_services(const char *managers)
{
	char **services = g_strsplit(managers, ",", 0);
	int ii;

	for(ii = 0; services && services[ii] ; ii++) {
		char *service = services[ii];
		char *path;
		struct modem_manager *manager;

		path = strchr(service, ':');
		if (path == NULL) {
			_DBG_MODEMMGR("Service %s missing path", service);
			continue;
		}
		*path = '\0';
		path++;

		manager = modem_manager_new(service, path);
		if (manager == NULL) {
			_DBG_MODEMMGR("Could not create manager: %s", service);
			continue;
		}
		manager->watch = g_dbus_add_service_watch(connection,
							  manager->service,
							  modemmgr_connect,
							  modemmgr_disconnect,
							  manager, NULL);
		if (manager->watch != 0) {
			g_hash_table_insert(modem_manager_watches,
					    manager->service, manager);
		} else {
			_DBG_MODEMMGR("Could not add service watch for %s",
				      service);
			modem_manager_free(manager);
		}
	}
	g_strfreev(services);
	return 0;
}


static void modemmgr_unwatch_service(gpointer key,
				     gpointer value,
				     gpointer userdata)
{
	struct modem_manager *manager = (struct modem_manager *)value;

	g_dbus_remove_watch(connection, manager->watch);
}

static void modemmgr_exit(void);

/**
 * modemmgr_init: Initialize the modem manager plugin.
 *
 * Set up a dbus connection to talk to modem managers
 * Set up a service watch to know when a modem manager starts and stops
 * Set up filters to receive signals from modem managers
 */
static int modemmgr_init(void)
{
	int err;

	connection = connman_dbus_get_connection();
	if (connection == NULL) {
		return -EIO;
	}
	/* We supply a free function for the value, but not the key,
	 * because the key is part of the value structure.
	 */
	modem_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
					   NULL, remove_modem);
	if (modem_hash == NULL) {
		dbus_connection_unref(connection);
		return -EIO;
	}
	modem_manager_watches = g_hash_table_new_full(g_str_hash, g_str_equal,
						      NULL,
						      modem_manager_free);
	if (modem_manager_watches == NULL) {
		goto remove;
	}

	if (dbus_connection_add_filter(connection,
				       modemmgr_filter, NULL, NULL) == FALSE) {
		_DBG_MODEMMGR("Could not add filter to connection %p",
			      connection);
		goto remove;
	}

	if (dbus_pending_call_allocate_data_slot(
		    &modem_pending_call_slot) == FALSE) {
		_DBG_MODEMMGR("Could not allocate pending call slot");
		goto remove;
	}

	/* TODO(jglasgow): add error handling on the dbus_connection_flush */
	dbus_bus_add_match(connection, modemmgr_rule, NULL);
	dbus_bus_add_match(connection, modem_rule, NULL);
	dbus_bus_add_match(connection, cdma_rule, NULL);
	dbus_bus_add_match(connection, gsm_network_rule, NULL);
	dbus_bus_add_match(connection, mm_properties_rule, NULL);
	dbus_connection_flush(connection);

	/* watch for connections from modem managers */
	modem_managers = connman_option_get_string("modemmanagers");
	if (modem_managers == NULL) {
		modem_managers = kDefaultMMSearchPath;
	}
	modemmgr_watch_mm_services(modem_managers);

	/* Register our drivers */
	err = connman_network_driver_register(&network_driver);
	if (err < 0)
		goto remove;

	err = connman_device_driver_register(&modem_driver);
	if (err < 0) {
		connman_network_driver_unregister(&network_driver);
		goto remove;
	}

	provider_db = mobile_provider_open_db(PROVIDERDB);
	if (provider_db == NULL)
		connman_warn("%s:%s() Cannot load mobile provider database: %s",
			     __FILE__, __FUNCTION__, strerror(errno));

	return 0;

remove:
	modemmgr_exit();
	return -EIO;
}


/**
 * modemmgr_exit: Clean up the modem manager plug in.
 */
static void modemmgr_exit(void)
{
	if (connection == NULL) {
		return;
	}
	if (modem_hash != NULL) {
		g_hash_table_destroy(modem_hash);
		modem_hash = NULL;
	}
	if (provider_db != NULL) {
		mobile_provider_close_db(provider_db);
		provider_db = NULL;
	}
	if (modem_manager_watches != NULL) {
		g_hash_table_foreach(modem_manager_watches,
				     modemmgr_unwatch_service, 0);
		g_hash_table_destroy(modem_manager_watches);
		modem_manager_watches = NULL;
	}
	if (modem_pending_call_slot != -1)
		dbus_pending_call_free_data_slot(&modem_pending_call_slot);
	dbus_bus_remove_match(connection, modemmgr_rule, NULL);
	dbus_bus_remove_match(connection, modem_rule, NULL);
	dbus_bus_remove_match(connection, cdma_rule, NULL);
	dbus_bus_remove_match(connection, gsm_network_rule, NULL);
	dbus_bus_remove_match(connection, mm_properties_rule, NULL);
	dbus_connection_remove_filter(connection, modemmgr_filter, NULL);
	dbus_connection_unref(connection);
	connection = NULL;

	connman_device_driver_unregister(&modem_driver);
	connman_network_driver_unregister(&network_driver);
}


CONNMAN_PLUGIN_DEFINE(modemmgr, "Modem Manager plugin", VERSION,
		      CONNMAN_PLUGIN_PRIORITY_DEFAULT,
		      modemmgr_init, modemmgr_exit)
